From dfd30ada280673c07565597888981f1a00f13eed Mon Sep 17 00:00:00 2001 From: Alexander Bulychev Date: Tue, 11 Jun 2024 10:18:26 +0400 Subject: [PATCH 01/32] NX: scripts configs for cloud caches (#27514) Co-authored-by: Alexander Bulychev --- .github/workflows/demos_unit_tests.yml | 2 +- .github/workflows/demos_visual_tests.yml | 2 +- .../demos_visual_tests_frameworks.yml | 10 +- .github/workflows/lint.yml | 41 +++--- .github/workflows/playgrounds_tests.yml | 8 +- .../qunit_tests-additional-renovation.yml | 8 +- .github/workflows/qunit_tests-renovation.yml | 7 +- .github/workflows/testcafe_tests.yml | 2 +- .github/workflows/themebuilder_tests.yml | 8 +- .github/workflows/wrapper_tests.yml | 19 ++- apps/angular/project.json | 25 ++++ apps/demos/.npmrc | 2 - apps/demos/package.json | 2 +- apps/demos/project.json | 119 ++++++++++++++++++ apps/react/project.json | 29 +++++ apps/vue/project.json | 29 +++++ e2e/testcafe-devextreme/project.json | 33 +++++ packages/devextreme-angular/project.json | 60 +++++++++ packages/devextreme-react/project.json | 42 +++++++ packages/devextreme-themebuilder/project.json | 16 +++ packages/devextreme-vue/project.json | 42 +++++++ packages/devextreme/project.json | 9 ++ 22 files changed, 464 insertions(+), 51 deletions(-) create mode 100644 apps/angular/project.json delete mode 100644 apps/demos/.npmrc create mode 100644 apps/demos/project.json create mode 100644 apps/react/project.json create mode 100644 apps/vue/project.json create mode 100644 e2e/testcafe-devextreme/project.json diff --git a/.github/workflows/demos_unit_tests.yml b/.github/workflows/demos_unit_tests.yml index 84daaea037da..85103ebcf63a 100644 --- a/.github/workflows/demos_unit_tests.yml +++ b/.github/workflows/demos_unit_tests.yml @@ -38,4 +38,4 @@ jobs: - name: Run unit tests working-directory: apps/demos - run: npm run test + run: npx nx test diff --git a/.github/workflows/demos_visual_tests.yml b/.github/workflows/demos_visual_tests.yml index 6a5596fccb71..b9464964659c 100644 --- a/.github/workflows/demos_visual_tests.yml +++ b/.github/workflows/demos_visual_tests.yml @@ -111,7 +111,7 @@ jobs: # DISABLE_DEMO_TEST_SETTINGS: ignore # Uncomment to ignore the `ignore` field # DISABLE_DEMO_TEST_SETTINGS: comparison-options # Uncomment to ignore the `comparison-options` field CI_ENV: true # The `ignore` field in the visualtestrc.json should be disabled when running test locally - run: npm run test-testcafe + run: npx nx test-testcafe - name: Show accessibility warnings if: matrix.STRATEGY == 'accessibility' diff --git a/.github/workflows/demos_visual_tests_frameworks.yml b/.github/workflows/demos_visual_tests_frameworks.yml index 4b5bbd661fa3..1be94efa97fa 100644 --- a/.github/workflows/demos_visual_tests_frameworks.yml +++ b/.github/workflows/demos_visual_tests_frameworks.yml @@ -11,6 +11,10 @@ on: - "[0-9][0-9]_[0-9]" workflow_dispatch: +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} + NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} + jobs: build-devextreme: runs-on: ubuntu-latest @@ -107,7 +111,7 @@ jobs: - name: Prepare bundles working-directory: apps/demos - run: npm run prepare-bundles + run: npx nx prepare-bundles - name: Demos - Run tsc working-directory: apps/demos @@ -160,7 +164,7 @@ jobs: matrix: include: - name: Lint code base (excluding demos) - command: npm run lint-non-demos + command: npx nx lint-non-demos # NOTE: skipped due to enormous number of errors # - name: Lint demos (1/4) # command: CONSTEL=1/4 npm run lint-demos @@ -378,7 +382,7 @@ jobs: # DISABLE_DEMO_TEST_SETTINGS: ignore # Uncomment to ignore the `ignore` field # DISABLE_DEMO_TEST_SETTINGS: comparison-options # Uncomment to ignore the `comparison-options` field CI_ENV: true # The `ignore` field in the visualtestrc.json should be disabled when running test locally - run: npm run test-testcafe + run: npx nx test-testcafe - name: Get screenshots artifacts name if: ${{ failure() }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d640cbf093e1..ab830ad0ff81 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,6 +9,10 @@ on: push: branches: [24_1] +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} + NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} + jobs: Renovation: runs-on: devextreme-shr2 @@ -35,12 +39,12 @@ jobs: - name: Compile renovation working-directory: ./packages/devextreme run: | - npm run compile:r + npx nx compile:r # Remove package install after upgrade to TypeScript >= 4.6 - name: Lint renovation working-directory: ./packages/devextreme - run: npm run lint-renovation + run: npx nx lint-renovation TS: runs-on: devextreme-shr2 @@ -66,25 +70,25 @@ jobs: - name: Build working-directory: ./packages/devextreme - run: npm run build + run: npx nx build - name: Lint TS working-directory: ./packages/devextreme env: DEBUG: eslint:cli-engine - run: npm run lint-ts + run: npx nx lint-ts - name: Lint .d.ts working-directory: ./packages/devextreme env: DEBUG: eslint:cli-engine - run: npm run lint-dts - + run: npx nx lint-dts + - name: Lint Testcafe tests working-directory: ./e2e/testcafe-devextreme env: DEBUG: eslint:cli-engine - run: npm run lint + run: npx nx lint JS: runs-on: devextreme-shr2 @@ -110,13 +114,13 @@ jobs: - name: Build working-directory: ./packages/devextreme - run: npm run build + run: npx nx build - name: Lint JS working-directory: ./packages/devextreme env: DEBUG: eslint:cli-engine - run: npm run lint-js + run: npx nx lint-js texts: runs-on: devextreme-shr2 @@ -142,7 +146,7 @@ jobs: - name: Check texts working-directory: ./packages/devextreme - run: npm run lint-texts + run: npx nx lint-texts CSS: runs-on: devextreme-shr2 @@ -168,7 +172,7 @@ jobs: - name: Lint CSS working-directory: ./packages/devextreme - run: npm run lint-css + run: npx nx lint-css package_lock: runs-on: devextreme-shr2 @@ -193,7 +197,7 @@ jobs: with: name: package-lock.json path: ./package-lock.json - retention-days: 1 + retention-days: 1 - name: Check package-lock run: git diff --exit-code package-lock.json @@ -260,17 +264,8 @@ jobs: - name: Install dependencies run: npm install --no-audit --no-fund - - name: Lint devextreme-angular - working-directory: ./packages/devextreme-angular - run: npm run lint - - - name: Lint devextreme-react - working-directory: ./packages/devextreme-react - run: npm run lint - - - name: Lint devextreme-vue - working-directory: ./packages/devextreme-vue - run: npm run lint + - name: Lint wrappers + run: npx nx run-many -t lint -p devextreme-angular devextreme-react devextreme-vue notify: runs-on: devextreme-shr2 diff --git a/.github/workflows/playgrounds_tests.yml b/.github/workflows/playgrounds_tests.yml index 921c4e52126e..77200ed5dd36 100644 --- a/.github/workflows/playgrounds_tests.yml +++ b/.github/workflows/playgrounds_tests.yml @@ -9,6 +9,10 @@ on: push: branches: [24_1] +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} + NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} + jobs: build: name: build @@ -39,7 +43,7 @@ jobs: - name: Build working-directory: ./packages/devextreme - run: npm run build + run: npx nx build - name: Zip artifacts working-directory: ./packages/devextreme @@ -106,7 +110,7 @@ jobs: - name: Check sources compilation working-directory: ./apps/${{ matrix.ARGS.platform }} - run: npm run build + run: npx nx build # - name: Run test # if: ${{ matrix.ARGS.platform != 'angular' }} diff --git a/.github/workflows/qunit_tests-additional-renovation.yml b/.github/workflows/qunit_tests-additional-renovation.yml index d53ffb720ace..5705f6a1e536 100644 --- a/.github/workflows/qunit_tests-additional-renovation.yml +++ b/.github/workflows/qunit_tests-additional-renovation.yml @@ -9,6 +9,10 @@ on: push: branches: [24_1] +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} + NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} + jobs: build: runs-on: devextreme-shr2 @@ -41,8 +45,8 @@ jobs: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" working-directory: ./packages/devextreme run: | - npm run build:dev - npm run build:systemjs + npx nx build:dev + npx nx build:systemjs - name: Zip artifacts working-directory: ./packages/devextreme diff --git a/.github/workflows/qunit_tests-renovation.yml b/.github/workflows/qunit_tests-renovation.yml index 50e703182139..aadbca641f50 100644 --- a/.github/workflows/qunit_tests-renovation.yml +++ b/.github/workflows/qunit_tests-renovation.yml @@ -9,6 +9,10 @@ on: push: branches: [24_1] +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} + NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} + jobs: build: runs-on: devextreme-shr2 @@ -40,7 +44,8 @@ jobs: DEVEXTREME_TEST_CI: "true" DOTNET_CLI_TELEMETRY_OPTOUT: "true" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" - run: BUILD_INPROGRESS_RENOVATION=true npm run build:dev + BUILD_INPROGRESS_RENOVATION: "true" + run: npx nx build:dev - name: Zip artifacts working-directory: ./packages/devextreme diff --git a/.github/workflows/testcafe_tests.yml b/.github/workflows/testcafe_tests.yml index 58eab1abb697..8d7cca54bec4 100644 --- a/.github/workflows/testcafe_tests.yml +++ b/.github/workflows/testcafe_tests.yml @@ -176,7 +176,7 @@ jobs: [ "${{ matrix.ARGS.platform }}" != "" ] && PLATFORM="--platform ${{ matrix.ARGS.platform }}" all_args="--browsers=chrome:devextreme-shr2 --componentFolder ${{ matrix.ARGS.componentFolder }} $CONCURRENCY $INDICES $PLATFORM $THEME" echo "$all_args" - npm run test -- $all_args + npx nx test $all_args - name: Copy compared screenshot artifacts if: ${{ failure() }} diff --git a/.github/workflows/themebuilder_tests.yml b/.github/workflows/themebuilder_tests.yml index 09b9c1b10662..887f31941f26 100644 --- a/.github/workflows/themebuilder_tests.yml +++ b/.github/workflows/themebuilder_tests.yml @@ -9,6 +9,10 @@ on: push: branches: [24_1] +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} + NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} + jobs: test: runs-on: devextreme-shr2 @@ -34,11 +38,11 @@ jobs: - name: Build working-directory: ./packages/devextreme-themebuilder - run: npm run build + run: npx nx build - name: Run themebuilder tests (full set, node) - run: npm run test working-directory: ./packages/devextreme-themebuilder + run: npx nx test - name: Check styles for duplicate rules (generic) working-directory: ./packages/devextreme diff --git a/.github/workflows/wrapper_tests.yml b/.github/workflows/wrapper_tests.yml index 9db74e46c95d..02386c6f5c68 100644 --- a/.github/workflows/wrapper_tests.yml +++ b/.github/workflows/wrapper_tests.yml @@ -8,6 +8,7 @@ on: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} + BUILD_TEST_INTERNAL_PACKAGE: true jobs: test: @@ -39,9 +40,7 @@ jobs: - name: Build devextreme package working-directory: ./packages/devextreme - env: - BUILD_TEST_INTERNAL_PACKAGE: true - run: npm run build + run: npx nx build - name: Generate wrappers run: npm run regenerate-all @@ -59,16 +58,13 @@ jobs: fi - name: Angular - Build - working-directory: ./packages/devextreme-angular - run: npm run build + run: npx nx build devextreme-angular - name: Angular - Run tests - working-directory: ./packages/devextreme-angular - run: npm run test:dev + run: npx nx test:dev devextreme-angular - name: Angular - Check packing - working-directory: ./packages/devextreme-angular - run: npm run pack + run: npx nx pack devextreme-angular - name: React - Run tests run: npx nx test devextreme-react @@ -77,11 +73,10 @@ jobs: run: npx nx pack devextreme-react - name: Vue - Run tests - working-directory: packages/devextreme-vue - run: npm run test + run: npx nx test devextreme-vue - name: Vue - Check packing - run: npx nx run-many -t pack -p devextreme-vue + run: npx nx pack devextreme-vue - name: Archive internal-tools artifacts uses: actions/upload-artifact@v3 diff --git a/apps/angular/project.json b/apps/angular/project.json new file mode 100644 index 000000000000..428af0a5dbda --- /dev/null +++ b/apps/angular/project.json @@ -0,0 +1,25 @@ +{ + "name": "devextreme-angular-playground", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/angular", + "projectType": "application", + "targets": { + "build": { + "executor": "nx:run-script", + "options": { + "script": "build" + }, + "inputs": [ + "{projectRoot}/src/**/*", + "{projectRoot}/angular.json", + "{projectRoot}/tsconfig*", + "{workspaceRoot}/tsconfig.json" + ], + "outputs": [ + "{projectRoot}/dist" + ], + "cache": true + } + }, + "tags": [] +} diff --git a/apps/demos/.npmrc b/apps/demos/.npmrc deleted file mode 100644 index 0d87f95153c8..000000000000 --- a/apps/demos/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -legacy-peer-deps = true -save-exact = true diff --git a/apps/demos/package.json b/apps/demos/package.json index 815cbbc75332..787350ce17b6 100644 --- a/apps/demos/package.json +++ b/apps/demos/package.json @@ -121,7 +121,7 @@ "lint-js": "eslint . --ignore-pattern 'Demos'", "lint-demos": "ts-node utils/eslint-runner", "lint-css": "stylelint **/*.{css,vue}", - "lint-non-demos": "npm-run-all -p -c lint-js lint-css lint-html", + "lint-non-demos": "npx nx run-many -t lint-js lint-css lint-html -p devextreme-demos", "lint": "npm-run-all -p -c lint-non-demos lint-demos", "test-testcafe": "node utils/visual-tests/testcafe-runner.mjs", "test-testcafe:accessibility": "cross-env STRATEGY=accessibility CONSTEL=jquery node utils/visual-tests/testcafe-runner.mjs", diff --git a/apps/demos/project.json b/apps/demos/project.json new file mode 100644 index 000000000000..1f43ad6fb57f --- /dev/null +++ b/apps/demos/project.json @@ -0,0 +1,119 @@ +{ + "name": "devextreme-demos", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/demos", + "projectType": "application", + "targets": { + "test": { + "executor": "nx:run-script", + "options": { + "script": "test" + }, + "dependsOn": [], + "inputs": [ + "default", + "test" + ], + "outputs": [ + "{projectRoot}/coverage" + ], + "cache": true + }, + "test-testcafe": { + "executor": "nx:run-script", + "options": { + "script": "test-testcafe" + }, + "dependsOn": [ + // NOTE: uncomment me when the NX cache is fixed to work in GHA + // "devextreme-main:build" + ], + "inputs": [ + { "env": "STRATEGY" }, + { "env": "CHANGEDFILEINFOSPATH" }, + { "env": "BROWSERS" }, + { "env": "DEBUG" }, + { "env": "TCQUARANTINE" }, + { "env": "CONSTEL" }, + { "env": "THEME" }, + { "env": "DISABLE_DEMO_TEST_SETTINGS" }, + { "env": "CI_ENV" }, + "default", + "test" + ], + "outputs": [ + "{projectRoot}/testing/artifacts" + ], + "cache": true + }, + "prepare-bundles": { + "executor": "nx:run-script", + "options": { + "script": "prepare-bundles" + }, + "dependsOn": [ + // "^build" uncomment me after migrating to PNPM + ], + "inputs": [ + "default", + "{projectRoot}/gulpfile.js/**/*", + "{projectRoot}/rollup.devextreme-angular.umd.config.mjs" + ], + "outputs": [ + "{projectRoot}/bundles" + ], + "cache": true + }, + "lint-js": { + "executor": "nx:run-script", + "options": { + "script": "lint-js" + }, + "inputs": [ + "default", + "!{projectRoot}/Demos/**/*", + "{projectRoot}/.eslint*" + ], + "cache": true + }, + "lint-css": { + "executor": "nx:run-script", + "options": { + "script": "lint-css" + }, + "inputs": [ + "{projectRoot}/Demos/**/*.css", + "{projectRoot}/Demos/**/*.vue", + "{projectRoot}/Demos/.stylelintrc.json" + ], + "cache": true + }, + "lint-html": { + "executor": "nx:run-script", + "options": { + "script": "lint-html" + }, + "inputs": [ + "{projectRoot}/**/*" + ], + "cache": true + } + }, + "namedInputs": { + "default": [ + "{projectRoot}/configs/**/*", + "{projectRoot}/data/**/*", + "{projectRoot}/Demos/**/*", + "{projectRoot}/images/**/*", + "{projectRoot}/shared/**/*", + "{projectRoot}/tsconfig.json" + ], + "test": [ + "{projectRoot}/testing/**/*", + "{projectRoot}/utils/**/*", + "{projectRoot}/babel.config.js", + "{projectRoot}/jest.config.js" + ] + }, + "tags": [] +} diff --git a/apps/react/project.json b/apps/react/project.json new file mode 100644 index 000000000000..da05292c2f24 --- /dev/null +++ b/apps/react/project.json @@ -0,0 +1,29 @@ +{ + "name": "devextreme-react-playground", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/react", + "projectType": "application", + "targets": { + "build": { + "executor": "nx:run-script", + "options": { + "script": "build" + }, + "inputs": [ + "{projectRoot}/examples/**/*", + "{projectRoot}/public/**/*", + "!{projectRoot}/public/js/app/bundle*", + "{projectRoot}/*.tsx", + "{projectRoot}/*.js", + "!{projectRoot}/test.js", + "{projectRoot}/tsconfig.json", + "{workspaceRoot}/tsconfig.json" + ], + "outputs": [ + "{projectRoot}/public/js/app/bundle*" + ], + "cache": true + } + }, + "tags": [] +} diff --git a/apps/vue/project.json b/apps/vue/project.json new file mode 100644 index 000000000000..73506830a0ef --- /dev/null +++ b/apps/vue/project.json @@ -0,0 +1,29 @@ +{ + "name": "devextreme-vue-playground", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/vue", + "projectType": "application", + "targets": { + "build": { + "executor": "nx:run-script", + "options": { + "script": "build" + }, + "inputs": [ + "{projectRoot}/components/**/*", + "{projectRoot}/public/**/*", + "!{projectRoot}/public/js/bundle*", + "{projectRoot}/*.ts", + "{projectRoot}/*.vue", + "{projectRoot}/*.js", + "!{projectRoot}/test.js", + "{projectRoot}/tsconfig.json" + ], + "outputs": [ + "{projectRoot}/public/js/bundle*" + ], + "cache": true + } + }, + "tags": [] +} diff --git a/e2e/testcafe-devextreme/project.json b/e2e/testcafe-devextreme/project.json new file mode 100644 index 000000000000..d8d511d94b33 --- /dev/null +++ b/e2e/testcafe-devextreme/project.json @@ -0,0 +1,33 @@ +{ + "name": "devextreme-testcafe-tests", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/testcafe-devextreme", + "projectType": "application", + "targets": { + "lint": { + "executor": "nx:run-script", + "options": { + "script": "lint" + }, + "inputs": [ + "{projectRoot}/**/*.js", + "{projectRoot}/**/*.ts" + ], + "cache": true + }, + "test": { + "executor": "nx:run-script", + "options": { + "script": "test" + }, + "inputs": [ + "^default", + "{projectRoot}/.testcaferc.json", + "{projectRoot}/runner.js", + "{projectRoot}/tsconfig.json" + ], + "cache": true + } + }, + "tags": [] +} diff --git a/packages/devextreme-angular/project.json b/packages/devextreme-angular/project.json index 0df473f23411..55768d10b40b 100644 --- a/packages/devextreme-angular/project.json +++ b/packages/devextreme-angular/project.json @@ -3,14 +3,74 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "packages/devextreme-angular", "projectType": "library", + "implicitDependencies": [ + "devextreme-main" + ], "targets": { + "build": { + "executor": "nx:run-script", + "options": { + "script": "build" + }, + "inputs": [ + "default" + ], + "outputs": [ + "{projectRoot}/dist", + "{projectRoot}/npm/dist" + ], + "cache": true + }, + "pack": { + "executor": "nx:run-script", + "options": { + "script": "pack" + }, + "inputs": [ + "default" + ], + "outputs": [ + "{projectRoot}/dist", + "{projectRoot}/npm/dist" + ], + "cache": true + }, "regenerate": { "executor": "nx:run-script", "options": { "script": "regenerate" }, "dependsOn": ["devextreme-monorepo:\"angular:copy-metadata\""] + }, + "test:dev": { + "executor": "nx:run-script", + "options": { + "script": "test:dev" + }, + "inputs": [ + "default", + "test" + ], + "cache": true } }, + "namedInputs": { + "default": [ + "{projectRoot}/metadata/**/*", + "{projectRoot}/src/**/*", + "{projectRoot}/build.config.js", + "{projectRoot}/gulpfile.js", + "{projectRoot}/ng-package.json", + "{projectRoot}/tsconfig.json", + "{projectRoot}/tsconfig.lib.json", + "{workspaceRoot}/tsconfig.json" + ], + "test": [ + "{projectRoot}/tests/**/*", + "{projectRoot}/karma*", + "{projectRoot}/tsconfig.tests.json", + "{projectRoot}/webpack.test.js" + ] + }, "tags": [] } diff --git a/packages/devextreme-react/project.json b/packages/devextreme-react/project.json index fae78ca42e8c..5db51b4b1c92 100644 --- a/packages/devextreme-react/project.json +++ b/packages/devextreme-react/project.json @@ -3,14 +3,56 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "packages/devextreme-react", "projectType": "library", + "implicitDependencies": [ + "devextreme-main" + ], "targets": { + "pack": { + "executor": "nx:run-script", + "options": { + "script": "pack" + }, + "inputs": [ + "default" + ], + "outputs": [ + "{projectRoot}/npm" + ], + "cache": true + }, "regenerate": { "executor": "nx:run-script", "options": { "script": "regenerate" }, "dependsOn": ["devextreme-monorepo:\"react:copy-metadata\""] + }, + "test": { + "executor": "nx:run-script", + "options": { + "script": "test" + }, + "inputs": [ + "default", + "test" + ], + "cache": true } }, + "namedInputs": { + "default": [ + "{projectRoot}/metadata/**/*", + "{projectRoot}/src/**/*", + "!{projectRoot}/src/**/__tests__/*", + "{projectRoot}/build.config.js", + "{projectRoot}/gulpfile.js", + "{projectRoot}/tsconfig*", + "{workspaceRoot}/tsconfig.json" + ], + "test": [ + "{projectRoot}/src/**/__tests__/*", + "{projectRoot}/jest.config.js" + ] + }, "tags": [] } diff --git a/packages/devextreme-themebuilder/project.json b/packages/devextreme-themebuilder/project.json index 9ce4e17f5d83..b536b5f90a4e 100644 --- a/packages/devextreme-themebuilder/project.json +++ b/packages/devextreme-themebuilder/project.json @@ -18,6 +18,22 @@ "{projectRoot}/src/data/metadata", "{projectRoot}/src/data/scss" ] + }, + "test": { + "executor": "nx:run-script", + "options": { + "script": "test" + }, + "inputs": [ + "{projectRoot}/src/**/*", + "{projectRoot}/tests/**/*", + "{projectRoot}/jest.config.js", + "{projectRoot}/tsconfig*" + ], + "outputs": [ + "{projectRoot}/coverage" + ], + "cache": true } }, "tags": [] diff --git a/packages/devextreme-vue/project.json b/packages/devextreme-vue/project.json index 1815ebf39983..04e86a7f6cfe 100644 --- a/packages/devextreme-vue/project.json +++ b/packages/devextreme-vue/project.json @@ -3,14 +3,56 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "packages/devextreme-vue", "projectType": "library", + "implicitDependencies": [ + "devextreme-main" + ], "targets": { + "pack": { + "executor": "nx:run-script", + "options": { + "script": "pack" + }, + "inputs": [ + "default" + ], + "outputs": [ + "{projectRoot}/npm" + ], + "cache": true + }, "regenerate": { "executor": "nx:run-script", "options": { "script": "regenerate" }, "dependsOn": ["devextreme-monorepo:\"vue:copy-metadata\""] + }, + "test": { + "executor": "nx:run-script", + "options": { + "script": "test" + }, + "inputs": [ + "default", + "test" + ], + "cache": true } }, + "namedInputs": { + "default": [ + "{projectRoot}/metadata/**/*", + "{projectRoot}/src/**/*", + "!{projectRoot}/src/**/__tests__/*", + "{projectRoot}/build.config.js", + "{projectRoot}/gulpfile.js", + "{projectRoot}/tsconfig*", + "{workspaceRoot}/tsconfig.json" + ], + "test": [ + "{projectRoot}/src/**/__tests__/*", + "{projectRoot}/jest.config.js" + ] + }, "tags": [] } diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 19a9950c0692..9688134e957b 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -16,6 +16,10 @@ ], "outputs": [ "{projectRoot}/artifacts", + "{projectRoot}/js/bundles/dx.custom.js", + "{projectRoot}/js/localization/cldr-data", + "{projectRoot}/js/localization/default_messages.js", + "{projectRoot}/js/renovation/**/*.j.tsx", "{projectRoot}/scss/bundles" ] }, @@ -26,6 +30,7 @@ }, "inputs": [ { "env": "BUILD_TEST_INTERNAL_PACKAGE" }, + { "env": "BUILD_INPROGRESS_RENOVATION" }, { "env": "DEVEXTREME_TEST_CI" }, { "env": "DOTNET_CLI_TELEMETRY_OPTOUT" }, { "env": "DOTNET_SKIP_FIRST_TIME_EXPERIENCE" }, @@ -34,6 +39,10 @@ ], "outputs": [ "{projectRoot}/artifacts", + "{projectRoot}/js/bundles/dx.custom.js", + "{projectRoot}/js/localization/cldr-data", + "{projectRoot}/js/localization/default_messages.js", + "{projectRoot}/js/renovation/**/*.j.tsx", "{projectRoot}/scss/bundles" ], "cache": true From cd694bdea1271c44774f298d4a8a1bc7c2c7ed05 Mon Sep 17 00:00:00 2001 From: Nikki Gonzales <38495263+nikkithelegendarypokemonster@users.noreply.github.com> Date: Tue, 11 Jun 2024 14:56:07 +0800 Subject: [PATCH 02/32] TabPanel: Remove extra div during panel creation in React framework (#27542) --- .../SortableClosableTabs/React/App.tsx | 2 -- .../SortableClosableTabs/ReactJs/App.js | 24 +++++++++---------- apps/demos/testing/common.test.js | 3 --- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/apps/demos/Demos/TabPanel/SortableClosableTabs/React/App.tsx b/apps/demos/Demos/TabPanel/SortableClosableTabs/React/App.tsx index 5774603c0ac7..68f315dc841e 100644 --- a/apps/demos/Demos/TabPanel/SortableClosableTabs/React/App.tsx +++ b/apps/demos/Demos/TabPanel/SortableClosableTabs/React/App.tsx @@ -39,12 +39,10 @@ function App() { const renderTitle = useCallback((data) => ( -
{data.FirstName} {data.LastName} {employees.length >= 2 && { closeButtonHandler(data); }} />} -
), [employees, closeButtonHandler]); diff --git a/apps/demos/Demos/TabPanel/SortableClosableTabs/ReactJs/App.js b/apps/demos/Demos/TabPanel/SortableClosableTabs/ReactJs/App.js index f2e5ff13ec91..021d311c9989 100644 --- a/apps/demos/Demos/TabPanel/SortableClosableTabs/ReactJs/App.js +++ b/apps/demos/Demos/TabPanel/SortableClosableTabs/ReactJs/App.js @@ -33,19 +33,17 @@ function App() { const renderTitle = useCallback( (data) => ( -
- - {data.FirstName} {data.LastName} - - {employees.length >= 2 && ( - { - closeButtonHandler(data); - }} - /> - )} -
+ + {data.FirstName} {data.LastName} + + {employees.length >= 2 && ( + { + closeButtonHandler(data); + }} + /> + )}
), [employees, closeButtonHandler], diff --git a/apps/demos/testing/common.test.js b/apps/demos/testing/common.test.js index 48ccfe55d0fc..230a28d04432 100644 --- a/apps/demos/testing/common.test.js +++ b/apps/demos/testing/common.test.js @@ -143,9 +143,6 @@ const SKIPPED_TESTS = { { demo: 'Overview', themes: [THEME.fluent, THEME.material] }, { demo: 'GroupByDate', themes: [THEME.fluent, THEME.material] }, ], - TabPanel: [ - { demo: 'SortableClosableTabs', themes: [THEME.fluent, THEME.material] }, - ], Toolbar: [ { demo: 'Adaptability', themes: [THEME.fluent, THEME.material] }, ], From a538ca711a00a03d23a013e761ee12e6dbb7dfff Mon Sep 17 00:00:00 2001 From: Roch Nicolas <89575880+tomodasheesh@users.noreply.github.com> Date: Tue, 11 Jun 2024 18:45:04 +0800 Subject: [PATCH 03/32] Update Trial Panel Buy Now Links (#27560) Co-authored-by: Rochmar Nicolas (DevExpress) --- .../devextreme-angular/src/core/component.ts | 6 ++++++ .../src/core/component-base.tsx | 20 +++++++++++------- packages/devextreme-vue/src/core/component.ts | 21 ++++++++++++------- .../core/license/license_validation.ts | 5 +++-- packages/devextreme/js/common.d.ts | 1 + packages/devextreme/ts/dx.all.d.ts | 1 + 6 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/devextreme-angular/src/core/component.ts b/packages/devextreme-angular/src/core/component.ts index 6302f4317229..2017b69d755e 100644 --- a/packages/devextreme-angular/src/core/component.ts +++ b/packages/devextreme-angular/src/core/component.ts @@ -22,6 +22,8 @@ import { isPlatformServer } from '@angular/common'; import domAdapter from 'devextreme/core/dom_adapter'; import { triggerHandler } from 'devextreme/events'; +import config from 'devextreme/core/config'; + import { DxTemplateDirective } from './template'; import { IDxTemplateHost, DxTemplateHost } from './template-host'; import { EmitterHelper, NgEventsStrategy } from './events-strategy'; @@ -34,6 +36,10 @@ import { CollectionNestedOptionContainerImpl, } from './nested-option'; +config({ + buyNowLink: 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeAngular.aspx', +}); + let serverStateKey; export const getServerStateKey = () => { if (!serverStateKey) { diff --git a/packages/devextreme-react/src/core/component-base.tsx b/packages/devextreme-react/src/core/component-base.tsx index bfec8577a932..99cb8fded199 100644 --- a/packages/devextreme-react/src/core/component-base.tsx +++ b/packages/devextreme-react/src/core/component-base.tsx @@ -14,6 +14,8 @@ import { import { requestAnimationFrame } from 'devextreme/animation/frame'; import { deferUpdate } from 'devextreme/core/utils/common'; +import config from 'devextreme/core/config'; + import { createPortal } from 'react-dom'; import { RemovalLockerContext, RestoreTreeContext } from './helpers'; @@ -26,6 +28,10 @@ import { ComponentProps } from './component'; const DX_REMOVE_EVENT = 'dxremove'; +config({ + buyNowLink: 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeReact.aspx', +}); + type ComponentBaseProps = ComponentProps & { renderChildren?: () => Record[] | null | undefined; }; @@ -201,14 +207,14 @@ const ComponentBase = forwardRef( el = el || element.current; - const config = getConfig(); + const widgetConfig = getConfig(); let options: any = { templatesRenderAsynchronously: true, - ...optionsManager.current.getInitialOptions(config), + ...optionsManager.current.getInitialOptions(widgetConfig), }; - const templateOptions = optionsManager.current.getTemplateOptions(config); + const templateOptions = optionsManager.current.getTemplateOptions(widgetConfig); const dxTemplates = createDXTemplates.current?.(templateOptions); if (dxTemplates && Object.keys(dxTemplates).length) { @@ -230,7 +236,7 @@ const ComponentBase = forwardRef( ); } - optionsManager.current.setInstance(instance.current, config, subscribableOptions, independentEvents); + optionsManager.current.setInstance(instance.current, widgetConfig, subscribableOptions, independentEvents); instance.current.on('optionChanged', optionsManager.current.onOptionChanged); afterCreateWidget(); @@ -257,12 +263,12 @@ const ComponentBase = forwardRef( updateCssClasses(prevPropsRef.current, props); - const config = getConfig(); + const widgetConfig = getConfig(); - const templateOptions = optionsManager.current.getTemplateOptions(config); + const templateOptions = optionsManager.current.getTemplateOptions(widgetConfig); const dxTemplates = createDXTemplates.current?.(templateOptions) || {}; - optionsManager.current.update(config, dxTemplates); + optionsManager.current.update(widgetConfig, dxTemplates); scheduleTemplatesUpdate(); prevPropsRef.current = props; diff --git a/packages/devextreme-vue/src/core/component.ts b/packages/devextreme-vue/src/core/component.ts index 1c8894bc9ea8..0945cc40b5f5 100644 --- a/packages/devextreme-vue/src/core/component.ts +++ b/packages/devextreme-vue/src/core/component.ts @@ -4,6 +4,7 @@ import { import CreateCallback from 'devextreme/core/utils/callbacks'; import { triggerHandler } from 'devextreme/events'; +import config from 'devextreme/core/config'; import { defaultSlots, getChildren, getComponentProps, getVModelValue, VMODEL_NAME, @@ -44,6 +45,10 @@ export interface IBaseComponent extends ComponentPublicInstance, IWidgetComponen const includeAttrs = ['id', 'class', 'style']; +config({ + buyNowLink: 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeVue.aspx', +}); + function getAttrs(attrs) { const attributes = {}; includeAttrs.forEach((attr) => { @@ -187,26 +192,26 @@ function initBaseComponent() { thisComponent.$_pendingOptions = {}; thisComponent.$_templatesManager = new TemplatesManager(this as ComponentPublicInstance); - const config = thisComponent.$_config; + const widgetConfig = thisComponent.$_config; - if (config.initialValues.hasOwnProperty(VMODEL_NAME)) { - config.initialValues.value = getVModelValue(config.initialValues); + if (widgetConfig.initialValues.hasOwnProperty(VMODEL_NAME)) { + widgetConfig.initialValues.value = getVModelValue(widgetConfig.initialValues); } const options: object = { templatesRenderAsynchronously: thisComponent.$_hasAsyncTemplate, ...getComponentProps(thisComponent), - ...config.initialValues, - ...config.getNestedOptionValues(), + ...widgetConfig.initialValues, + ...widgetConfig.getNestedOptionValues(), ...this.$_getIntegrationOptions(), }; const instance = new thisComponent.$_WidgetClass(element, options); thisComponent.$_instance = instance; - instance.on('optionChanged', (args) => config.onOptionChanged(args)); - setEmitOptionChangedFunc(config, thisComponent, thisComponent.$_innerChanges); - bindOptionWatchers(config, thisComponent, thisComponent.$_innerChanges); + instance.on('optionChanged', (args) => widgetConfig.onOptionChanged(args)); + setEmitOptionChangedFunc(widgetConfig, thisComponent, thisComponent.$_innerChanges); + bindOptionWatchers(widgetConfig, thisComponent, thisComponent.$_innerChanges); this.$_createEmitters(instance); }, diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 6ff342aff827..fd1981bb7ce0 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -1,3 +1,4 @@ +import config from '@js/core/config'; import errors from '@js/core/errors'; import { fullVersion } from '@js/core/version'; @@ -30,7 +31,7 @@ const FORMAT = 1; const RTM_MIN_PATCH_VERSION = 3; const KEY_SPLITTER = '.'; -const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtreme.aspx'; +const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; const GENERAL_ERROR: Token = { kind: TokenKind.corrupted, error: 'general' }; const VERIFICATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'verification' }; @@ -193,7 +194,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } if (error && !internal) { - showTrialPanel(BUY_NOW_LINK, fullVersion); + showTrialPanel(config().buyNowLink ?? BUY_NOW_LINK, fullVersion); } const preview = isPreview(version.patch); diff --git a/packages/devextreme/js/common.d.ts b/packages/devextreme/js/common.d.ts index 7d7227b91e2b..7f6c1794e592 100644 --- a/packages/devextreme/js/common.d.ts +++ b/packages/devextreme/js/common.d.ts @@ -387,6 +387,7 @@ export type GlobalConfig = { * @public */ licenseKey?: string; + buyNowLink?: string; }; /** diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index ad1b638e6b40..11e096d667b6 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -1545,6 +1545,7 @@ declare module DevExpress.common { * [descr:GlobalConfig.licenseKey] */ licenseKey?: string; + buyNowLink?: string; }; /** * [descr:GroupItem] From c8e496a2e69ea8bf8025b6e4b1dcc794b08e4ef4 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 6 Jun 2024 18:28:44 +0400 Subject: [PATCH 04/32] Tabs, TabPanel: move files to TS --- .../{ui/tab_panel/item.js => __internal/ui/tab_panel/m_item.ts} | 0 .../{ui/tab_panel.js => __internal/ui/tab_panel/m_tab_panel.ts} | 0 .../js/{ui/tabs/constants.js => __internal/ui/tabs/constants.ts} | 0 .../js/{ui/tabs/item.js => __internal/ui/tabs/m_item.ts} | 0 .../devextreme/js/{ui/tabs.js => __internal/ui/tabs/m_tabs.ts} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/tab_panel/item.js => __internal/ui/tab_panel/m_item.ts} (100%) rename packages/devextreme/js/{ui/tab_panel.js => __internal/ui/tab_panel/m_tab_panel.ts} (100%) rename packages/devextreme/js/{ui/tabs/constants.js => __internal/ui/tabs/constants.ts} (100%) rename packages/devextreme/js/{ui/tabs/item.js => __internal/ui/tabs/m_item.ts} (100%) rename packages/devextreme/js/{ui/tabs.js => __internal/ui/tabs/m_tabs.ts} (100%) diff --git a/packages/devextreme/js/ui/tab_panel/item.js b/packages/devextreme/js/__internal/ui/tab_panel/m_item.ts similarity index 100% rename from packages/devextreme/js/ui/tab_panel/item.js rename to packages/devextreme/js/__internal/ui/tab_panel/m_item.ts diff --git a/packages/devextreme/js/ui/tab_panel.js b/packages/devextreme/js/__internal/ui/tab_panel/m_tab_panel.ts similarity index 100% rename from packages/devextreme/js/ui/tab_panel.js rename to packages/devextreme/js/__internal/ui/tab_panel/m_tab_panel.ts diff --git a/packages/devextreme/js/ui/tabs/constants.js b/packages/devextreme/js/__internal/ui/tabs/constants.ts similarity index 100% rename from packages/devextreme/js/ui/tabs/constants.js rename to packages/devextreme/js/__internal/ui/tabs/constants.ts diff --git a/packages/devextreme/js/ui/tabs/item.js b/packages/devextreme/js/__internal/ui/tabs/m_item.ts similarity index 100% rename from packages/devextreme/js/ui/tabs/item.js rename to packages/devextreme/js/__internal/ui/tabs/m_item.ts diff --git a/packages/devextreme/js/ui/tabs.js b/packages/devextreme/js/__internal/ui/tabs/m_tabs.ts similarity index 100% rename from packages/devextreme/js/ui/tabs.js rename to packages/devextreme/js/__internal/ui/tabs/m_tabs.ts From d03b6f2f219b4f7dcd3c2f5681183fc3437eec47 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 6 Jun 2024 18:51:32 +0400 Subject: [PATCH 05/32] Tabs, TabPanel: ignore errors after move to TS --- .../js/__internal/ui/tab_panel/m_item.ts | 14 +- .../js/__internal/ui/tab_panel/m_tab_panel.ts | 977 ++++++------ .../js/__internal/ui/tabs/m_item.ts | 34 +- .../js/__internal/ui/tabs/m_tabs.ts | 1324 ++++++++--------- packages/devextreme/js/ui/tab_panel.js | 16 + packages/devextreme/js/ui/tabs.js | 17 + 6 files changed, 1195 insertions(+), 1187 deletions(-) create mode 100644 packages/devextreme/js/ui/tab_panel.js create mode 100644 packages/devextreme/js/ui/tabs.js diff --git a/packages/devextreme/js/__internal/ui/tab_panel/m_item.ts b/packages/devextreme/js/__internal/ui/tab_panel/m_item.ts index 25c9c11df2da..f6558412855e 100644 --- a/packages/devextreme/js/__internal/ui/tab_panel/m_item.ts +++ b/packages/devextreme/js/__internal/ui/tab_panel/m_item.ts @@ -1,10 +1,12 @@ -import CollectionWidgetItem from '../collection/item'; -import { noop } from '../../core/utils/common'; +import { noop } from '@js/core/utils/common'; +import CollectionWidgetItem from '@js/ui/collection/item'; export default class TabPanelItem extends CollectionWidgetItem { - _renderWatchers() { - this._startWatcher('badge', noop); + _renderWatchers() { + // @ts-expect-error + this._startWatcher('badge', noop); - return super._renderWatchers(); - } + // @ts-expect-error + return super._renderWatchers(); + } } diff --git a/packages/devextreme/js/__internal/ui/tab_panel/m_tab_panel.ts b/packages/devextreme/js/__internal/ui/tab_panel/m_tab_panel.ts index ab317925a039..0f3d5637dee8 100644 --- a/packages/devextreme/js/__internal/ui/tab_panel/m_tab_panel.ts +++ b/packages/devextreme/js/__internal/ui/tab_panel/m_tab_panel.ts @@ -1,19 +1,19 @@ -import $ from '../core/renderer'; -import { touch } from '../core/utils/support'; -import { extend } from '../core/utils/extend'; -import devices from '../core/devices'; -import domAdapter from '../core/dom_adapter'; -import registerComponent from '../core/component_registrator'; -import MultiView from './multi_view'; -import Tabs from './tabs'; -import { default as TabPanelItem } from './tab_panel/item'; -import { getImageContainer } from '../core/utils/icon'; -import { getPublicElement } from '../core/element'; -import { isPlainObject, isDefined } from '../core/utils/type'; -import { BindableTemplate } from '../core/templates/bindable_template'; -import { isMaterialBased, isFluent, current as currentTheme } from './themes'; - -// STYLE tabPanel +import registerComponent from '@js/core/component_registrator'; +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import { getPublicElement } from '@js/core/element'; +import $ from '@js/core/renderer'; +import { BindableTemplate } from '@js/core/templates/bindable_template'; +import { extend } from '@js/core/utils/extend'; +import { getImageContainer } from '@js/core/utils/icon'; +import { touch } from '@js/core/utils/support'; +import { isDefined, isPlainObject } from '@js/core/utils/type'; +import MultiView from '@js/ui/multi_view'; +import Tabs from '@js/ui/tabs'; +import { current as currentTheme, isFluent, isMaterialBased } from '@js/ui/themes'; + +// eslint-disable-next-line import/no-named-default +import { default as TabPanelItem } from './m_item'; const TABPANEL_CLASS = 'dx-tabpanel'; const TABPANEL_TABS_CLASS = 'dx-tabpanel-tabs'; @@ -25,509 +25,494 @@ const TABS_ITEM_TEXT_SPAN_CLASS = 'dx-tab-text-span'; const TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS = 'dx-tab-text-span-pseudo'; const TABPANEL_TABS_POSITION_CLASS = { - top: 'dx-tabpanel-tabs-position-top', - right: 'dx-tabpanel-tabs-position-right', - bottom: 'dx-tabpanel-tabs-position-bottom', - left: 'dx-tabpanel-tabs-position-left', + top: 'dx-tabpanel-tabs-position-top', + right: 'dx-tabpanel-tabs-position-right', + bottom: 'dx-tabpanel-tabs-position-bottom', + left: 'dx-tabpanel-tabs-position-left', }; const TABS_POSITION = { - top: 'top', - right: 'right', - bottom: 'bottom', - left: 'left', + top: 'top', + right: 'right', + bottom: 'bottom', + left: 'left', }; const TABS_INDICATOR_POSITION_BY_TABS_POSITION = { - top: 'bottom', - right: 'left', - bottom: 'top', - left: 'right', + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right', }; const TABS_ORIENTATION = { - horizontal: 'horizontal', - vertical: 'vertical', + horizontal: 'horizontal', + vertical: 'vertical', }; const ICON_POSITION = { - top: 'top', - end: 'end', - bottom: 'bottom', - start: 'start', + top: 'top', + end: 'end', + bottom: 'bottom', + start: 'start', }; const STYLING_MODE = { - primary: 'primary', - secondary: 'secondary', + primary: 'primary', + secondary: 'secondary', }; +// @ts-expect-error const TabPanel = MultiView.inherit({ - _getDefaultOptions: function() { - return extend(this.callBase(), { - - - itemTitleTemplate: 'title', - - hoverStateEnabled: true, - - showNavButtons: false, - - scrollByContent: true, - - scrollingEnabled: true, - - tabsPosition: TABS_POSITION.top, - - iconPosition: ICON_POSITION.start, - - stylingMode: STYLING_MODE.primary, - - onTitleClick: null, - - onTitleHold: null, - - onTitleRendered: null, - - badgeExpr: function(data) { return data ? data.badge : undefined; }, - - /** - * @name dxTabPanelItem.visible - * @hidden - */ - - _tabsIndicatorPosition: null, - }); - }, - - _defaultOptionsRules: function() { - const themeName = currentTheme(); - - return this.callBase().concat([ - { - device: function() { - return devices.real().deviceType === 'desktop' && !devices.isSimulator(); - }, - options: { - focusStateEnabled: true - } - }, - { - device: function() { - return !touch; - }, - options: { - swipeEnabled: false - } - }, - { - device: { platform: 'generic' }, - options: { - animationEnabled: false - } - }, - { - device() { - return isFluent(themeName); - }, - options: { - stylingMode: STYLING_MODE.secondary, - } - }, - { - device() { - return isMaterialBased(themeName); - }, - options: { - iconPosition: ICON_POSITION.top, - } - } - ]); - }, - - _init: function() { - this.callBase(); - - this.$element().addClass(TABPANEL_CLASS); - this._toggleTabPanelTabsPositionClass(); - }, - - _getElementAria() { - return { role: 'tabpanel' }; - }, - - _getItemAria() { - return { role: 'tabpanel' }; - }, - - _initMarkup: function() { - this.callBase(); - - this._createTitleActions(); - this._renderLayout(); - }, - - _prepareTabsItemTemplate(data, $container) { - const $iconElement = getImageContainer(data?.icon); - - if($iconElement) { - $container.append($iconElement); - } - - const title = isPlainObject(data) ? data?.title : data; - - if(isDefined(title) && !isPlainObject(title)) { - const $tabTextSpan = $('').addClass(TABS_ITEM_TEXT_SPAN_CLASS); - - $tabTextSpan.append(domAdapter.createTextNode(title)); - - const $tabTextSpanPseudo = $('').addClass(TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS); - - $tabTextSpanPseudo.append(domAdapter.createTextNode(title)); - $tabTextSpanPseudo.appendTo($tabTextSpan); - - $tabTextSpan.appendTo($container); - } - }, - - _initTemplates() { - this.callBase(); - - this._templateManager.addDefaultTemplates({ - title: new BindableTemplate(($container, data) => { - this._prepareTabsItemTemplate(data, $container); - - const $tabItem = $('
').addClass(TABS_ITEM_TEXT_CLASS); - - $container.wrapInner($tabItem); - }, ['title', 'icon'], this.option('integrationOptions.watchMethod')) - }); - }, - - _createTitleActions: function() { - this._createTitleClickAction(); - this._createTitleHoldAction(); - this._createTitleRenderedAction(); - }, - - _createTitleClickAction: function() { - this._titleClickAction = this._createActionByOption('onTitleClick'); - }, - - _createTitleHoldAction: function() { - this._titleHoldAction = this._createActionByOption('onTitleHold'); - }, - - _createTitleRenderedAction: function() { - this._titleRenderedAction = this._createActionByOption('onTitleRendered'); - }, - - _renderLayout: function() { - if(this._tabs) { - return; + _getDefaultOptions() { + return extend(this.callBase(), { + itemTitleTemplate: 'title', + hoverStateEnabled: true, + showNavButtons: false, + scrollByContent: true, + scrollingEnabled: true, + tabsPosition: TABS_POSITION.top, + iconPosition: ICON_POSITION.start, + stylingMode: STYLING_MODE.primary, + onTitleClick: null, + onTitleHold: null, + onTitleRendered: null, + badgeExpr(data) { return data ? data.badge : undefined; }, + + _tabsIndicatorPosition: null, + }); + }, + + _defaultOptionsRules() { + const themeName = currentTheme(); + + return this.callBase().concat([ + { + device() { + return devices.real().deviceType === 'desktop' && !devices.isSimulator(); + }, + options: { + focusStateEnabled: true, + }, + }, + { + device() { + return !touch; + }, + options: { + swipeEnabled: false, + }, + }, + { + device: { platform: 'generic' }, + options: { + animationEnabled: false, + }, + }, + { + device() { + return isFluent(themeName); + }, + options: { + stylingMode: STYLING_MODE.secondary, + }, + }, + { + device() { + return isMaterialBased(themeName); + }, + options: { + iconPosition: ICON_POSITION.top, + }, + }, + ]); + }, + + _init() { + this.callBase(); + + this.$element().addClass(TABPANEL_CLASS); + this._toggleTabPanelTabsPositionClass(); + }, + + _getElementAria() { + return { role: 'tabpanel' }; + }, + + _getItemAria() { + return { role: 'tabpanel' }; + }, + + _initMarkup() { + this.callBase(); + + this._createTitleActions(); + this._renderLayout(); + }, + + _prepareTabsItemTemplate(data, $container) { + const $iconElement = getImageContainer(data?.icon); + + if ($iconElement) { + $container.append($iconElement); + } + + const title = isPlainObject(data) ? data?.title : data; + + if (isDefined(title) && !isPlainObject(title)) { + const $tabTextSpan = $('').addClass(TABS_ITEM_TEXT_SPAN_CLASS); + // @ts-expect-error + $tabTextSpan.append(domAdapter.createTextNode(title)); + + const $tabTextSpanPseudo = $('').addClass(TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS); + // @ts-expect-error + $tabTextSpanPseudo.append(domAdapter.createTextNode(title)); + $tabTextSpanPseudo.appendTo($tabTextSpan); + + $tabTextSpan.appendTo($container); + } + }, + + _initTemplates() { + this.callBase(); + + this._templateManager.addDefaultTemplates({ + title: new BindableTemplate(($container, data) => { + this._prepareTabsItemTemplate(data, $container); + + const $tabItem = $('
').addClass(TABS_ITEM_TEXT_CLASS); + + $container.wrapInner($tabItem); + }, ['title', 'icon'], this.option('integrationOptions.watchMethod')), + }); + }, + + _createTitleActions() { + this._createTitleClickAction(); + this._createTitleHoldAction(); + this._createTitleRenderedAction(); + }, + + _createTitleClickAction() { + this._titleClickAction = this._createActionByOption('onTitleClick'); + }, + + _createTitleHoldAction() { + this._titleHoldAction = this._createActionByOption('onTitleHold'); + }, + + _createTitleRenderedAction() { + this._titleRenderedAction = this._createActionByOption('onTitleRendered'); + }, + + _renderLayout() { + if (this._tabs) { + return; + } + + const $element = this.$element(); + + this._$tabContainer = $('
') + .addClass(TABPANEL_TABS_CLASS) + .appendTo($element); + + const $tabs = $('
').appendTo(this._$tabContainer); + + this._tabs = this._createComponent($tabs, Tabs, this._tabConfig()); + + this._$container = $('
') + .addClass(TABPANEL_CONTAINER_CLASS) + .appendTo($element); + this._$container.append(this._$wrapper); + }, + + _refreshActiveDescendant() { + if (!this._tabs) { + return; + } + + const tabs = this._tabs; + const tabItems = tabs.itemElements(); + const $activeTab = $(tabItems[tabs.option('selectedIndex')]); + const id = this.getFocusedItemId(); + + this.setAria('controls', undefined, $(tabItems)); + this.setAria('controls', id, $activeTab); + }, + + _getTabsIndicatorPosition() { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { _tabsIndicatorPosition, tabsPosition } = this.option(); + + return _tabsIndicatorPosition ?? TABS_INDICATOR_POSITION_BY_TABS_POSITION[tabsPosition]; + }, + + _tabConfig() { + const tabsIndicatorPosition = this._getTabsIndicatorPosition(); + + return { + selectOnFocus: true, + focusStateEnabled: this.option('focusStateEnabled'), + hoverStateEnabled: this.option('hoverStateEnabled'), + repaintChangesOnly: this.option('repaintChangesOnly'), + tabIndex: this.option('tabIndex'), + selectedIndex: this.option('selectedIndex'), + badgeExpr: this.option('badgeExpr'), + onItemClick: this._titleClickAction.bind(this), + onItemHold: this._titleHoldAction.bind(this), + itemHoldTimeout: this.option('itemHoldTimeout'), + onSelectionChanged: function (e) { + this.option('selectedIndex', e.component.option('selectedIndex')); + this._refreshActiveDescendant(); + }.bind(this), + onItemRendered: this._titleRenderedAction.bind(this), + itemTemplate: this._getTemplateByOption('itemTitleTemplate'), + items: this.option('items'), + noDataText: null, + scrollingEnabled: this.option('scrollingEnabled'), + scrollByContent: this.option('scrollByContent'), + showNavButtons: this.option('showNavButtons'), + itemTemplateProperty: 'tabTemplate', + loopItemFocus: this.option('loop'), + selectionRequired: true, + onOptionChanged: function (args) { + if (args.name === 'focusedElement') { + if (args.value) { + const $value = $(args.value); + const $newItem = this._itemElements().eq($value.index()); + this.option('focusedElement', getPublicElement($newItem)); + } else { + this.option('focusedElement', args.value); + } } - - const $element = this.$element(); - - this._$tabContainer = $('
') - .addClass(TABPANEL_TABS_CLASS) - .appendTo($element); - - const $tabs = $('
').appendTo(this._$tabContainer); - - this._tabs = this._createComponent($tabs, Tabs, this._tabConfig()); - - this._$container = $('
') - .addClass(TABPANEL_CONTAINER_CLASS) - .appendTo($element); - this._$container.append(this._$wrapper); - }, - - _refreshActiveDescendant: function() { - if(!this._tabs) { - return; - } - - const tabs = this._tabs; - const tabItems = tabs.itemElements(); - const $activeTab = $(tabItems[tabs.option('selectedIndex')]); - const id = this.getFocusedItemId(); - - this.setAria('controls', undefined, $(tabItems)); - this.setAria('controls', id, $activeTab); - }, - - _getTabsIndicatorPosition() { - const { _tabsIndicatorPosition, tabsPosition } = this.option(); - - return _tabsIndicatorPosition ?? TABS_INDICATOR_POSITION_BY_TABS_POSITION[tabsPosition]; - }, - - _tabConfig() { - const tabsIndicatorPosition = this._getTabsIndicatorPosition(); - - return { - selectOnFocus: true, - focusStateEnabled: this.option('focusStateEnabled'), - hoverStateEnabled: this.option('hoverStateEnabled'), - repaintChangesOnly: this.option('repaintChangesOnly'), - tabIndex: this.option('tabIndex'), - selectedIndex: this.option('selectedIndex'), - badgeExpr: this.option('badgeExpr'), - onItemClick: this._titleClickAction.bind(this), - onItemHold: this._titleHoldAction.bind(this), - itemHoldTimeout: this.option('itemHoldTimeout'), - onSelectionChanged: (function(e) { - this.option('selectedIndex', e.component.option('selectedIndex')); - this._refreshActiveDescendant(); - }).bind(this), - onItemRendered: this._titleRenderedAction.bind(this), - itemTemplate: this._getTemplateByOption('itemTitleTemplate'), - items: this.option('items'), - noDataText: null, - scrollingEnabled: this.option('scrollingEnabled'), - scrollByContent: this.option('scrollByContent'), - showNavButtons: this.option('showNavButtons'), - itemTemplateProperty: 'tabTemplate', - loopItemFocus: this.option('loop'), - selectionRequired: true, - onOptionChanged: (function(args) { - if(args.name === 'focusedElement') { - if(args.value) { - const $value = $(args.value); - const $newItem = this._itemElements().eq($value.index()); - this.option('focusedElement', getPublicElement($newItem)); - } else { - this.option('focusedElement', args.value); - } - } - }).bind(this), - onFocusIn: (function(args) { this._focusInHandler(args.event); }).bind(this), - onFocusOut: (function(args) { - if(!this._isFocusOutHandlerExecuting) { - this._focusOutHandler(args.event); - } - }).bind(this), - orientation: this._getTabsOrientation(), - iconPosition: this.option('iconPosition'), - stylingMode: this.option('stylingMode'), - _itemAttributes: { class: TABPANEL_TABS_ITEM_CLASS }, - _indicatorPosition: tabsIndicatorPosition, - }; - }, - - _renderFocusTarget: function() { - this._focusTarget().attr('tabIndex', -1); - }, - - _getTabsOrientation() { - const { tabsPosition } = this.option(); - - if([TABS_POSITION.right, TABS_POSITION.left].includes(tabsPosition)) { - return TABS_ORIENTATION.vertical; + }.bind(this), + onFocusIn: function (args) { this._focusInHandler(args.event); }.bind(this), + onFocusOut: function (args) { + if (!this._isFocusOutHandlerExecuting) { + this._focusOutHandler(args.event); } - - return TABS_ORIENTATION.horizontal; - }, - - _getTabPanelTabsPositionClass() { - const position = this.option('tabsPosition'); - - switch(position) { - case TABS_POSITION.right: - return TABPANEL_TABS_POSITION_CLASS.right; - case TABS_POSITION.bottom: - return TABPANEL_TABS_POSITION_CLASS.bottom; - case TABS_POSITION.left: - return TABPANEL_TABS_POSITION_CLASS.left; - case TABS_POSITION.top: - default: - return TABPANEL_TABS_POSITION_CLASS.top; + }.bind(this), + orientation: this._getTabsOrientation(), + iconPosition: this.option('iconPosition'), + stylingMode: this.option('stylingMode'), + _itemAttributes: { class: TABPANEL_TABS_ITEM_CLASS }, + _indicatorPosition: tabsIndicatorPosition, + }; + }, + + _renderFocusTarget() { + this._focusTarget().attr('tabIndex', -1); + }, + + _getTabsOrientation() { + const { tabsPosition } = this.option(); + + if ([TABS_POSITION.right, TABS_POSITION.left].includes(tabsPosition)) { + return TABS_ORIENTATION.vertical; + } + + return TABS_ORIENTATION.horizontal; + }, + + _getTabPanelTabsPositionClass() { + const position = this.option('tabsPosition'); + + switch (position) { + case TABS_POSITION.right: + return TABPANEL_TABS_POSITION_CLASS.right; + case TABS_POSITION.bottom: + return TABPANEL_TABS_POSITION_CLASS.bottom; + case TABS_POSITION.left: + return TABPANEL_TABS_POSITION_CLASS.left; + case TABS_POSITION.top: + default: + return TABPANEL_TABS_POSITION_CLASS.top; + } + }, + + _toggleTabPanelTabsPositionClass() { + // eslint-disable-next-line guard-for-in, no-restricted-syntax + for (const key in TABPANEL_TABS_POSITION_CLASS) { + this.$element().removeClass(TABPANEL_TABS_POSITION_CLASS[key]); + } + + const newClass = this._getTabPanelTabsPositionClass(); + + this.$element().addClass(newClass); + }, + + _updateTabsOrientation() { + const orientation = this._getTabsOrientation(); + + this._setTabsOption('orientation', orientation); + }, + + _toggleWrapperFocusedClass(isFocused) { + this._toggleFocusClass(isFocused, this._$wrapper); + }, + + _toggleDisabledFocusedClass(isFocused) { + this._focusTarget().toggleClass(DISABLED_FOCUSED_TAB_CLASS, isFocused); + }, + + _updateFocusState(e, isFocused) { + this.callBase(e, isFocused); + + const isTabsTarget = e.target === this._tabs._focusTarget().get(0); + const isMultiViewTarget = e.target === this._focusTarget().get(0); + + if (isTabsTarget) { + this._toggleFocusClass(isFocused, this._focusTarget()); + } + + if (isTabsTarget || isMultiViewTarget) { + const isDisabled = this._isDisabled(this.option('focusedElement')); + + this._toggleWrapperFocusedClass(isFocused && !isDisabled); + this._toggleDisabledFocusedClass(isFocused && isDisabled); + } + + if (isMultiViewTarget) { + this._toggleFocusClass(isFocused, this._tabs.option('focusedElement')); + } + }, + + _focusOutHandler(e) { + this._isFocusOutHandlerExecuting = true; + + this.callBase.apply(this, arguments); + + this._tabs._focusOutHandler(e); + this._isFocusOutHandlerExecuting = false; + }, + + _setTabsOption(name, value) { + if (this._tabs) { + this._tabs.option(name, value); + } + }, + + _visibilityChanged(visible) { + if (visible) { + this._tabs._dimensionChanged(); + } + }, + + registerKeyHandler(key, handler) { + this.callBase(key, handler); + + if (this._tabs) { + this._tabs.registerKeyHandler(key, handler); + } + }, + + repaint() { + this.callBase(); + this._tabs.repaint(); + }, + + _updateTabsIndicatorPosition() { + const value = this._getTabsIndicatorPosition(); + + this._setTabsOption('_indicatorPosition', value); + }, + + _optionChanged(args) { + const { name, value, fullName } = args; + + switch (name) { + case 'dataSource': + this.callBase(args); + break; + case 'items': + this._setTabsOption(name, this.option(name)); + if (!this.option('repaintChangesOnly')) { + this._tabs.repaint(); } - }, - - _toggleTabPanelTabsPositionClass() { - for(const key in TABPANEL_TABS_POSITION_CLASS) { - this.$element().removeClass(TABPANEL_TABS_POSITION_CLASS[key]); - } - - const newClass = this._getTabPanelTabsPositionClass(); - - this.$element().addClass(newClass); - }, - - _updateTabsOrientation() { - const orientation = this._getTabsOrientation(); - - this._setTabsOption('orientation', orientation); - }, - - _toggleWrapperFocusedClass(isFocused) { - this._toggleFocusClass(isFocused, this._$wrapper); - }, - - _toggleDisabledFocusedClass(isFocused) { - this._focusTarget().toggleClass(DISABLED_FOCUSED_TAB_CLASS, isFocused); - }, - - _updateFocusState: function(e, isFocused) { - this.callBase(e, isFocused); - - const isTabsTarget = e.target === this._tabs._focusTarget().get(0); - const isMultiViewTarget = e.target === this._focusTarget().get(0); - - if(isTabsTarget) { - this._toggleFocusClass(isFocused, this._focusTarget()); - } - - if(isTabsTarget || isMultiViewTarget) { - const isDisabled = this._isDisabled(this.option('focusedElement')); - - this._toggleWrapperFocusedClass(isFocused && !isDisabled); - this._toggleDisabledFocusedClass(isFocused && isDisabled); - } - - if(isMultiViewTarget) { - this._toggleFocusClass(isFocused, this._tabs.option('focusedElement')); - } - }, - - _focusOutHandler: function(e) { - this._isFocusOutHandlerExecuting = true; - - this.callBase.apply(this, arguments); - - this._tabs._focusOutHandler(e); - this._isFocusOutHandlerExecuting = false; - }, - - _setTabsOption(name, value) { - if(this._tabs) { - this._tabs.option(name, value); - } - }, - - _visibilityChanged: function(visible) { - if(visible) { - this._tabs._dimensionChanged(); + this.callBase(args); + break; + case 'width': + this.callBase(args); + this._tabs.repaint(); + break; + case 'selectedIndex': + case 'selectedItem': { + this._setTabsOption(fullName, value); + this.callBase(args); + + if (this.option('focusStateEnabled') === true) { + const selectedIndex = this.option('selectedIndex'); + const selectedTabContent = this._itemElements().eq(selectedIndex); + this.option('focusedElement', getPublicElement(selectedTabContent)); } - }, - - registerKeyHandler: function(key, handler) { - this.callBase(key, handler); - - if(this._tabs) { - this._tabs.registerKeyHandler(key, handler); + break; + } + case 'itemHoldTimeout': + case 'focusStateEnabled': + case 'hoverStateEnabled': + this._setTabsOption(fullName, value); + this.callBase(args); + break; + case 'scrollingEnabled': + case 'scrollByContent': + case 'showNavButtons': + this._setTabsOption(fullName, value); + break; + case 'focusedElement': { + const id = value ? $(value).index() : value; + const newItem = value ? this._tabs._itemElements().eq(id) : value; + this._setTabsOption('focusedElement', getPublicElement(newItem)); + + if (value) { + const isDisabled = this._isDisabled(value); + + this._toggleWrapperFocusedClass(!isDisabled); + this._toggleDisabledFocusedClass(isDisabled); } - }, - - repaint: function() { - this.callBase(); - this._tabs.repaint(); - }, - - _updateTabsIndicatorPosition() { - const value = this._getTabsIndicatorPosition(); + this.callBase(args); + break; + } + case 'itemTitleTemplate': + this._setTabsOption('itemTemplate', this._getTemplateByOption('itemTitleTemplate')); + break; + case 'onTitleClick': + this._createTitleClickAction(); + this._setTabsOption('onItemClick', this._titleClickAction.bind(this)); + break; + case 'onTitleHold': + this._createTitleHoldAction(); + this._setTabsOption('onItemHold', this._titleHoldAction.bind(this)); + break; + case 'onTitleRendered': + this._createTitleRenderedAction(); + this._setTabsOption('onItemRendered', this._titleRenderedAction.bind(this)); + break; + case 'loop': + this._setTabsOption('loopItemFocus', value); + break; + case 'badgeExpr': + this._invalidate(); + break; + case 'tabsPosition': + this._toggleTabPanelTabsPositionClass(); + this._updateTabsIndicatorPosition(); + this._updateTabsOrientation(); + break; + case 'iconPosition': + this._setTabsOption('iconPosition', value); + break; + case 'stylingMode': + this._setTabsOption('stylingMode', value); + break; + case '_tabsIndicatorPosition': this._setTabsOption('_indicatorPosition', value); - }, - - _optionChanged: function(args) { - const { name, value, fullName } = args; - - switch(name) { - case 'dataSource': - this.callBase(args); - break; - case 'items': - this._setTabsOption(name, this.option(name)); - if(!this.option('repaintChangesOnly')) { - this._tabs.repaint(); - } - this.callBase(args); - break; - case 'width': - this.callBase(args); - this._tabs.repaint(); - break; - case 'selectedIndex': - case 'selectedItem': { - this._setTabsOption(fullName, value); - this.callBase(args); - - if(this.option('focusStateEnabled') === true) { - const selectedIndex = this.option('selectedIndex'); - const selectedTabContent = this._itemElements().eq(selectedIndex); - this.option('focusedElement', getPublicElement(selectedTabContent)); - } - break; - } - case 'itemHoldTimeout': - case 'focusStateEnabled': - case 'hoverStateEnabled': - this._setTabsOption(fullName, value); - this.callBase(args); - break; - case 'scrollingEnabled': - case 'scrollByContent': - case 'showNavButtons': - this._setTabsOption(fullName, value); - break; - case 'focusedElement': { - const id = value ? $(value).index() : value; - const newItem = value ? this._tabs._itemElements().eq(id) : value; - this._setTabsOption('focusedElement', getPublicElement(newItem)); - - if(value) { - const isDisabled = this._isDisabled(value); - - this._toggleWrapperFocusedClass(!isDisabled); - this._toggleDisabledFocusedClass(isDisabled); - } - - this.callBase(args); - break; - } - case 'itemTitleTemplate': - this._setTabsOption('itemTemplate', this._getTemplateByOption('itemTitleTemplate')); - break; - case 'onTitleClick': - this._createTitleClickAction(); - this._setTabsOption('onItemClick', this._titleClickAction.bind(this)); - break; - case 'onTitleHold': - this._createTitleHoldAction(); - this._setTabsOption('onItemHold', this._titleHoldAction.bind(this)); - break; - case 'onTitleRendered': - this._createTitleRenderedAction(); - this._setTabsOption('onItemRendered', this._titleRenderedAction.bind(this)); - break; - case 'loop': - this._setTabsOption('loopItemFocus', value); - break; - case 'badgeExpr': - this._invalidate(); - break; - case 'tabsPosition': - this._toggleTabPanelTabsPositionClass(); - this._updateTabsIndicatorPosition(); - this._updateTabsOrientation(); - break; - case 'iconPosition': - this._setTabsOption('iconPosition', value); - break; - case 'stylingMode': - this._setTabsOption('stylingMode', value); - break; - case '_tabsIndicatorPosition': - this._setTabsOption('_indicatorPosition', value); - break; - default: - this.callBase(args); - } - }, + break; + default: + this.callBase(args); + } + }, }); TabPanel.ItemClass = TabPanelItem; @@ -535,9 +520,3 @@ TabPanel.ItemClass = TabPanelItem; registerComponent('dxTabPanel', TabPanel); export default TabPanel; - -/** - * @name dxTabPanelItem - * @inherits dxMultiViewItem - * @type object - */ diff --git a/packages/devextreme/js/__internal/ui/tabs/m_item.ts b/packages/devextreme/js/__internal/ui/tabs/m_item.ts index 92e600389dbc..abd64568a529 100644 --- a/packages/devextreme/js/__internal/ui/tabs/m_item.ts +++ b/packages/devextreme/js/__internal/ui/tabs/m_item.ts @@ -1,31 +1,31 @@ -import $ from '../../core/renderer'; -import CollectionWidgetItem from '../collection/item'; +import $ from '@js/core/renderer'; +import CollectionWidgetItem from '@js/ui/collection/item'; const TABS_ITEM_BADGE_CLASS = 'dx-tabs-item-badge'; const BADGE_CLASS = 'dx-badge'; const TabsItem = CollectionWidgetItem.inherit({ - _renderWatchers: function() { - this.callBase(); + _renderWatchers() { + this.callBase(); - this._startWatcher('badge', this._renderBadge.bind(this)); - }, + this._startWatcher('badge', this._renderBadge.bind(this)); + }, - _renderBadge: function(badge) { - this._$element.children('.' + BADGE_CLASS).remove(); + _renderBadge(badge) { + this._$element.children(`.${BADGE_CLASS}`).remove(); - if(!badge) { - return; - } + if (!badge) { + return; + } - const $badge = $('
') - .addClass(TABS_ITEM_BADGE_CLASS) - .addClass(BADGE_CLASS) - .text(badge); + const $badge = $('
') + .addClass(TABS_ITEM_BADGE_CLASS) + .addClass(BADGE_CLASS) + .text(badge); - this._$element.append($badge); - } + this._$element.append($badge); + }, }); diff --git a/packages/devextreme/js/__internal/ui/tabs/m_tabs.ts b/packages/devextreme/js/__internal/ui/tabs/m_tabs.ts index a414f9365d14..9078d822e7a6 100644 --- a/packages/devextreme/js/__internal/ui/tabs/m_tabs.ts +++ b/packages/devextreme/js/__internal/ui/tabs/m_tabs.ts @@ -1,29 +1,31 @@ -import { getWidth, getHeight, getOuterWidth } from '../core/utils/size'; -import $ from '../core/renderer'; -import eventsEngine from '../events/core/events_engine'; -import devices from '../core/devices'; -import registerComponent from '../core/component_registrator'; -import Button from './button'; -import { render } from './widget/utils.ink_ripple'; -import { addNamespace } from '../events/utils/index'; -import { extend } from '../core/utils/extend'; -import { isPlainObject, isDefined } from '../core/utils/type'; -import pointerEvents from '../events/pointer'; -import { each } from '../core/utils/iterator'; -import TabsItem from './tabs/item'; -import { TABS_EXPANDED_CLASS } from './tabs/constants'; -import { isMaterial, isFluent, current as currentTheme } from './themes'; -import holdEvent from '../events/hold'; -import Scrollable from './scroll_view/ui.scrollable'; -import { default as CollectionWidget } from './collection/ui.collection_widget.live_update'; -import { getImageContainer } from '../core/utils/icon'; -import { BindableTemplate } from '../core/templates/bindable_template'; -import { Deferred, when } from '../core/utils/deferred'; -import { isReachedLeft, isReachedRight, isReachedTop, isReachedBottom } from '../renovation/ui/scroll_view/utils/get_boundary_props'; -import { getScrollLeftMax } from '../renovation/ui/scroll_view/utils/get_scroll_left_max'; -import { hasWindow } from '../core/utils/window'; - -// STYLE tabs +import registerComponent from '@js/core/component_registrator'; +import devices from '@js/core/devices'; +import $ from '@js/core/renderer'; +import { BindableTemplate } from '@js/core/templates/bindable_template'; +import { Deferred, when } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { getImageContainer } from '@js/core/utils/icon'; +import { each } from '@js/core/utils/iterator'; +import { getHeight, getOuterWidth, getWidth } from '@js/core/utils/size'; +import { isDefined, isPlainObject } from '@js/core/utils/type'; +import { hasWindow } from '@js/core/utils/window'; +import eventsEngine from '@js/events/core/events_engine'; +import holdEvent from '@js/events/hold'; +import pointerEvents from '@js/events/pointer'; +import { addNamespace } from '@js/events/utils/index'; +import { + isReachedBottom, isReachedLeft, isReachedRight, isReachedTop, +} from '@js/renovation/ui/scroll_view/utils/get_boundary_props'; +import { getScrollLeftMax } from '@js/renovation/ui/scroll_view/utils/get_scroll_left_max'; +import Button from '@js/ui/button'; +// eslint-disable-next-line import/no-named-default +import { default as CollectionWidget } from '@js/ui/collection/ui.collection_widget.live_update'; +import Scrollable from '@js/ui/scroll_view/ui.scrollable'; +import { current as currentTheme, isFluent, isMaterial } from '@js/ui/themes'; +import { render } from '@js/ui/widget/utils.ink_ripple'; + +import { TABS_EXPANDED_CLASS } from './constants'; +import TabsItem from './m_item'; const TABS_CLASS = 'dx-tabs'; const TABS_WRAPPER_CLASS = 'dx-tabs-wrapper'; @@ -49,27 +51,27 @@ const FOCUSED_DISABLED_NEXT_TAB_CLASS = 'dx-focused-disabled-next-tab'; const FOCUSED_DISABLED_PREV_TAB_CLASS = 'dx-focused-disabled-prev-tab'; const TABS_ORIENTATION_CLASS = { - vertical: 'dx-tabs-vertical', - horizontal: 'dx-tabs-horizontal', + vertical: 'dx-tabs-vertical', + horizontal: 'dx-tabs-horizontal', }; const INDICATOR_POSITION_CLASS = { - top: 'dx-tab-indicator-position-top', - right: 'dx-tab-indicator-position-right', - bottom: 'dx-tab-indicator-position-bottom', - left: 'dx-tab-indicator-position-left', + top: 'dx-tab-indicator-position-top', + right: 'dx-tab-indicator-position-right', + bottom: 'dx-tab-indicator-position-bottom', + left: 'dx-tab-indicator-position-left', }; const TABS_ICON_POSITION_CLASS = { - top: 'dx-tabs-icon-position-top', - end: 'dx-tabs-icon-position-end', - bottom: 'dx-tabs-icon-position-bottom', - start: 'dx-tabs-icon-position-start', + top: 'dx-tabs-icon-position-top', + end: 'dx-tabs-icon-position-end', + bottom: 'dx-tabs-icon-position-bottom', + start: 'dx-tabs-icon-position-start', }; const TABS_STYLING_MODE_CLASS = { - primary: 'dx-tabs-styling-mode-primary', - secondary: 'dx-tabs-styling-mode-secondary', + primary: 'dx-tabs-styling-mode-primary', + secondary: 'dx-tabs-styling-mode-secondary', }; const TABS_ITEM_DATA_KEY = 'dxTabData'; @@ -84,732 +86,730 @@ const FEEDBACK_SCROLL_TIMEOUT = 300; const TAB_OFFSET = 30; const ORIENTATION = { - horizontal: 'horizontal', - vertical: 'vertical', + horizontal: 'horizontal', + vertical: 'vertical', }; const INDICATOR_POSITION = { - top: 'top', - right: 'right', - bottom: 'bottom', - left: 'left', + top: 'top', + right: 'right', + bottom: 'bottom', + left: 'left', }; const SCROLLABLE_DIRECTION = { - horizontal: 'horizontal', - vertical: 'vertical', + horizontal: 'horizontal', + vertical: 'vertical', }; const ICON_POSITION = { - top: 'top', - end: 'end', - bottom: 'bottom', - start: 'start', + top: 'top', + end: 'end', + bottom: 'bottom', + start: 'start', }; const STYLING_MODE = { - primary: 'primary', - secondary: 'secondary', + primary: 'primary', + secondary: 'secondary', }; - const Tabs = CollectionWidget.inherit({ - _activeStateUnit: '.' + TABS_ITEM_CLASS, - - _getDefaultOptions: function() { - return extend(this.callBase(), { - hoverStateEnabled: true, - showNavButtons: true, - scrollByContent: true, - scrollingEnabled: true, - selectionMode: 'single', - orientation: ORIENTATION.horizontal, - iconPosition: ICON_POSITION.start, - stylingMode: STYLING_MODE.primary, - - /** - * @name dxTabsOptions.activeStateEnabled - * @hidden - * @default true - */ - - activeStateEnabled: true, - selectionRequired: false, - selectOnFocus: true, - loopItemFocus: false, - useInkRipple: false, - badgeExpr: function(data) { return data ? data.badge : undefined; }, - _itemAttributes: { role: 'tab' }, - _indicatorPosition: null, - }); - }, - - _defaultOptionsRules: function() { - const themeName = currentTheme(); - - return this.callBase().concat([ - { - device() { - return devices.real().deviceType !== 'desktop'; - }, - options: { - showNavButtons: false - } - }, - { - device: { deviceType: 'desktop' }, - options: { - scrollByContent: false - } - }, - { - device() { - return devices.real().deviceType === 'desktop' && !devices.isSimulator(); - }, - options: { - focusStateEnabled: true - } - }, - { - device() { - return isFluent(themeName); - }, - options: { - iconPosition: ICON_POSITION.top, - stylingMode: STYLING_MODE.secondary, - } - }, - { - device() { - return isMaterial(themeName); - }, - options: { - useInkRipple: true, - selectOnFocus: false, - iconPosition: ICON_POSITION.top, - } - } - ]); - }, - - _init() { - const { orientation, stylingMode, scrollingEnabled } = this.option(); - const indicatorPosition = this._getIndicatorPosition(); - - this.callBase(); - this.setAria('role', 'tablist'); - this.$element().addClass(TABS_CLASS); - this._toggleScrollingEnabledClass(scrollingEnabled); - this._toggleOrientationClass(orientation); - this._toggleIndicatorPositionClass(indicatorPosition); - this._toggleIconPositionClass(); - this._toggleStylingModeClass(stylingMode); - this._renderWrapper(); - this._renderMultiple(); - - this._feedbackHideTimeout = FEEDBACK_HIDE_TIMEOUT; - }, - - _prepareDefaultItemTemplate(data, $container) { - const text = isPlainObject(data) ? data?.text : data; - - if(isDefined(text)) { - const $tabTextSpan = $('').addClass(TABS_ITEM_TEXT_SPAN_CLASS); - - $tabTextSpan.text(text); - - const $tabTextSpanPseudo = $('').addClass(TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS); - - $tabTextSpanPseudo.text(text); - $tabTextSpanPseudo.appendTo($tabTextSpan); - - $tabTextSpan.appendTo($container); - } + _activeStateUnit: `.${TABS_ITEM_CLASS}`, + + _getDefaultOptions() { + return extend(this.callBase(), { + hoverStateEnabled: true, + showNavButtons: true, + scrollByContent: true, + scrollingEnabled: true, + selectionMode: 'single', + orientation: ORIENTATION.horizontal, + iconPosition: ICON_POSITION.start, + stylingMode: STYLING_MODE.primary, + activeStateEnabled: true, + selectionRequired: false, + selectOnFocus: true, + loopItemFocus: false, + useInkRipple: false, + badgeExpr(data) { return data ? data.badge : undefined; }, + _itemAttributes: { role: 'tab' }, + _indicatorPosition: null, + }); + }, + + _defaultOptionsRules() { + const themeName = currentTheme(); + + return this.callBase().concat([ + { + device() { + return devices.real().deviceType !== 'desktop'; + }, + options: { + showNavButtons: false, + }, + }, + { + device: { deviceType: 'desktop' }, + options: { + scrollByContent: false, + }, + }, + { + device() { + return devices.real().deviceType === 'desktop' && !devices.isSimulator(); + }, + options: { + focusStateEnabled: true, + }, + }, + { + device() { + return isFluent(themeName); + }, + options: { + iconPosition: ICON_POSITION.top, + stylingMode: STYLING_MODE.secondary, + }, + }, + { + device() { + return isMaterial(themeName); + }, + options: { + useInkRipple: true, + selectOnFocus: false, + iconPosition: ICON_POSITION.top, + }, + }, + ]); + }, + + _init() { + const { orientation, stylingMode, scrollingEnabled } = this.option(); + const indicatorPosition = this._getIndicatorPosition(); + + this.callBase(); + this.setAria('role', 'tablist'); + this.$element().addClass(TABS_CLASS); + this._toggleScrollingEnabledClass(scrollingEnabled); + this._toggleOrientationClass(orientation); + this._toggleIndicatorPositionClass(indicatorPosition); + this._toggleIconPositionClass(); + this._toggleStylingModeClass(stylingMode); + this._renderWrapper(); + this._renderMultiple(); + + this._feedbackHideTimeout = FEEDBACK_HIDE_TIMEOUT; + }, + + _prepareDefaultItemTemplate(data, $container) { + const text = isPlainObject(data) ? data?.text : data; + + if (isDefined(text)) { + const $tabTextSpan = $('').addClass(TABS_ITEM_TEXT_SPAN_CLASS); + + $tabTextSpan.text(text); + + const $tabTextSpanPseudo = $('').addClass(TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS); + + $tabTextSpanPseudo.text(text); + $tabTextSpanPseudo.appendTo($tabTextSpan); + + $tabTextSpan.appendTo($container); + } - if(isDefined(data.html)) { - $container.html(data.html); - } - }, - - _initTemplates() { - this.callBase(); - - this._templateManager.addDefaultTemplates({ - item: new BindableTemplate((($container, data) => { - this._prepareDefaultItemTemplate(data, $container); - - const $iconElement = getImageContainer(data.icon); - $iconElement && $iconElement.prependTo($container); - - const $tabItem = $('
').addClass(TABS_ITEM_TEXT_CLASS); - - $container.wrapInner($tabItem); - }).bind(this), ['text', 'html', 'icon'], this.option('integrationOptions.watchMethod')) - }); - }, - - _createItemByTemplate: function _createItemByTemplate(itemTemplate, renderArgs) { - const { itemData, container, index } = renderArgs; - this._deferredTemplates[index] = new Deferred(); - return itemTemplate.render({ - model: itemData, - container, - index, - onRendered: () => this._deferredTemplates[index].resolve() - }); - }, - - _itemClass: function() { - return TABS_ITEM_CLASS; - }, - - _selectedItemClass: function() { - return TABS_ITEM_SELECTED_CLASS; - }, - - _itemDataKey: function() { - return TABS_ITEM_DATA_KEY; - }, - - _initMarkup: function() { - this._deferredTemplates = []; - this.callBase(); - - this.option('useInkRipple') && this._renderInkRipple(); - - this.$element().addClass(OVERFLOW_HIDDEN_CLASS); - }, - - _render: function() { - this.callBase(); - this._deferRenderScrolling(); - }, - - _deferRenderScrolling() { - when.apply(this, this._deferredTemplates).done(() => this._renderScrolling()); - }, - - _renderScrolling() { - const removeClasses = [TABS_STRETCHED_CLASS, TABS_EXPANDED_CLASS, OVERFLOW_HIDDEN_CLASS]; - this.$element().removeClass(removeClasses.join(' ')); - - if(this.option('scrollingEnabled') && this._isItemsSizeExceeded()) { - if(!this._scrollable) { - this._renderScrollable(); - this._renderNavButtons(); - } - - const scrollable = this.getScrollable(); - scrollable.update(); - - if(this.option('rtlEnabled')) { - const maxLeftOffset = getScrollLeftMax($(this.getScrollable().container()).get(0)); - scrollable.scrollTo({ left: maxLeftOffset }); - } - this._updateNavButtonsState(); - - this._scrollToItem(this.option('selectedItem')); - } + if (isDefined(data.html)) { + $container.html(data.html); + } + }, + + _initTemplates() { + this.callBase(); + + this._templateManager.addDefaultTemplates({ + item: new BindableTemplate(($container, data) => { + this._prepareDefaultItemTemplate(data, $container); + + const $iconElement = getImageContainer(data.icon); + $iconElement && $iconElement.prependTo($container); + + const $tabItem = $('
').addClass(TABS_ITEM_TEXT_CLASS); + + $container.wrapInner($tabItem); + }, ['text', 'html', 'icon'], this.option('integrationOptions.watchMethod')), + }); + }, + + // eslint-disable-next-line @typescript-eslint/naming-convention + _createItemByTemplate: function _createItemByTemplate(itemTemplate, renderArgs) { + const { itemData, container, index } = renderArgs; + this._deferredTemplates[index] = Deferred(); + return itemTemplate.render({ + model: itemData, + container, + index, + onRendered: () => this._deferredTemplates[index].resolve(), + }); + }, + + _itemClass() { + return TABS_ITEM_CLASS; + }, + + _selectedItemClass() { + return TABS_ITEM_SELECTED_CLASS; + }, + + _itemDataKey() { + return TABS_ITEM_DATA_KEY; + }, + + _initMarkup() { + this._deferredTemplates = []; + this.callBase(); + + this.option('useInkRipple') && this._renderInkRipple(); + + this.$element().addClass(OVERFLOW_HIDDEN_CLASS); + }, + + _render() { + this.callBase(); + this._deferRenderScrolling(); + }, + + _deferRenderScrolling() { + when.apply(this, this._deferredTemplates).done(() => this._renderScrolling()); + }, + + _renderScrolling() { + const removeClasses = [TABS_STRETCHED_CLASS, TABS_EXPANDED_CLASS, OVERFLOW_HIDDEN_CLASS]; + this.$element().removeClass(removeClasses.join(' ')); + + if (this.option('scrollingEnabled') && this._isItemsSizeExceeded()) { + if (!this._scrollable) { + this._renderScrollable(); + this._renderNavButtons(); + } + + const scrollable = this.getScrollable(); + scrollable.update(); + + if (this.option('rtlEnabled')) { + // @ts-expect-error + const maxLeftOffset = getScrollLeftMax($(this.getScrollable().container()).get(0)); + scrollable.scrollTo({ left: maxLeftOffset }); + } + this._updateNavButtonsState(); + + this._scrollToItem(this.option('selectedItem')); + } - if(!(this.option('scrollingEnabled') && this._isItemsSizeExceeded())) { - this._cleanScrolling(); + if (!(this.option('scrollingEnabled') && this._isItemsSizeExceeded())) { + this._cleanScrolling(); - if(this._needStretchItems()) { - this.$element().addClass(TABS_STRETCHED_CLASS); - } + if (this._needStretchItems()) { + this.$element().addClass(TABS_STRETCHED_CLASS); + } - this.$element() - .removeClass(TABS_NAV_BUTTONS_CLASS) - .addClass(TABS_EXPANDED_CLASS); - } - }, + this.$element() + .removeClass(TABS_NAV_BUTTONS_CLASS) + .addClass(TABS_EXPANDED_CLASS); + } + }, - _isVertical() { - return this.option('orientation') === ORIENTATION.vertical; - }, + _isVertical() { + return this.option('orientation') === ORIENTATION.vertical; + }, - _isItemsSizeExceeded() { - const isVertical = this._isVertical(); - const isItemsSizeExceeded = isVertical ? this._isItemsHeightExceeded() : this._isItemsWidthExceeded(); + _isItemsSizeExceeded() { + const isVertical = this._isVertical(); + const isItemsSizeExceeded = isVertical ? this._isItemsHeightExceeded() : this._isItemsWidthExceeded(); - return isItemsSizeExceeded; - }, + return isItemsSizeExceeded; + }, - _isItemsWidthExceeded() { - const $visibleItems = this._getVisibleItems(); - const tabItemTotalWidth = this._getSummaryItemsSize('width', $visibleItems, true); - const elementWidth = getWidth(this.$element()); + _isItemsWidthExceeded() { + const $visibleItems = this._getVisibleItems(); + const tabItemTotalWidth = this._getSummaryItemsSize('width', $visibleItems, true); + const elementWidth = getWidth(this.$element()); - if([tabItemTotalWidth, elementWidth].includes(0)) { - return false; - } + if ([tabItemTotalWidth, elementWidth].includes(0)) { + return false; + } - const isItemsWidthExceeded = tabItemTotalWidth > elementWidth - 1; + const isItemsWidthExceeded = tabItemTotalWidth > elementWidth - 1; - return isItemsWidthExceeded; - }, + return isItemsWidthExceeded; + }, - _isItemsHeightExceeded() { - const $visibleItems = this._getVisibleItems(); - const itemsHeight = this._getSummaryItemsSize('height', $visibleItems, true); - const elementHeight = getHeight(this.$element()); - const isItemsHeightExceeded = itemsHeight - 1 > elementHeight; + _isItemsHeightExceeded() { + const $visibleItems = this._getVisibleItems(); + const itemsHeight = this._getSummaryItemsSize('height', $visibleItems, true); + const elementHeight = getHeight(this.$element()); + const isItemsHeightExceeded = itemsHeight - 1 > elementHeight; - return isItemsHeightExceeded; - }, + return isItemsHeightExceeded; + }, - _needStretchItems() { - const $visibleItems = this._getVisibleItems(); - const elementWidth = getWidth(this.$element()); - const itemsWidth = []; + _needStretchItems() { + const $visibleItems = this._getVisibleItems(); + const elementWidth = getWidth(this.$element()); + const itemsWidth = []; - each($visibleItems, (_, item) => { - itemsWidth.push(getOuterWidth(item, true)); - }); + each($visibleItems, (_, item) => { + // @ts-expect-error + itemsWidth.push(getOuterWidth(item, true)); + }); - const maxTabItemWidth = Math.max.apply(null, itemsWidth); - const requireWidth = elementWidth / $visibleItems.length; - const needStretchItems = maxTabItemWidth > requireWidth + 1; + const maxTabItemWidth = Math.max.apply(null, itemsWidth); + const requireWidth = elementWidth / $visibleItems.length; + const needStretchItems = maxTabItemWidth > requireWidth + 1; - return needStretchItems; - }, + return needStretchItems; + }, - _cleanNavButtons: function() { - if(!this._leftButton || !this._rightButton) return; + _cleanNavButtons() { + if (!this._leftButton || !this._rightButton) return; - this._leftButton.$element().remove(); - this._rightButton.$element().remove(); - this._leftButton = null; - this._rightButton = null; - }, + this._leftButton.$element().remove(); + this._rightButton.$element().remove(); + this._leftButton = null; + this._rightButton = null; + }, - _cleanScrolling: function() { - if(!this._scrollable) return; + _cleanScrolling() { + if (!this._scrollable) return; - this._$wrapper.appendTo(this.$element()); + this._$wrapper.appendTo(this.$element()); - this._scrollable.$element().remove(); - this._scrollable = null; + this._scrollable.$element().remove(); + this._scrollable = null; - this._cleanNavButtons(); - }, + this._cleanNavButtons(); + }, - _renderInkRipple: function() { - this._inkRipple = render(); - }, + _renderInkRipple() { + this._inkRipple = render(); + }, - _getPointerEvent() { - return pointerEvents.up; - }, + _getPointerEvent() { + return pointerEvents.up; + }, - _toggleActiveState: function($element, value, e) { - this.callBase.apply(this, arguments); + _toggleActiveState($element, value, e) { + this.callBase.apply(this, arguments); - if(!this._inkRipple) { - return; - } + if (!this._inkRipple) { + return; + } - const config = { - element: $element, - event: e - }; + const config = { + element: $element, + event: e, + }; - if(value) { - this._inkRipple.showWave(config); - } else { - this._inkRipple.hideWave(config); - } - }, + if (value) { + this._inkRipple.showWave(config); + } else { + this._inkRipple.hideWave(config); + } + }, - _renderMultiple: function() { - if(this.option('selectionMode') === 'multiple') { - this.option('selectOnFocus', false); - } - }, + _renderMultiple() { + if (this.option('selectionMode') === 'multiple') { + this.option('selectOnFocus', false); + } + }, - _renderWrapper: function() { - this._$wrapper = $('
').addClass(TABS_WRAPPER_CLASS); - this.$element().append(this._$wrapper); - }, + _renderWrapper() { + this._$wrapper = $('
').addClass(TABS_WRAPPER_CLASS); + this.$element().append(this._$wrapper); + }, - _itemContainer: function() { - return this._$wrapper; - }, + _itemContainer() { + return this._$wrapper; + }, - _getScrollableDirection() { - const isVertical = this._isVertical(); - const scrollableDirection = isVertical ? SCROLLABLE_DIRECTION.vertical : SCROLLABLE_DIRECTION.horizontal; + _getScrollableDirection() { + const isVertical = this._isVertical(); + const scrollableDirection = isVertical ? SCROLLABLE_DIRECTION.vertical : SCROLLABLE_DIRECTION.horizontal; - return scrollableDirection; - }, + return scrollableDirection; + }, - _updateScrollable() { - if(this.getScrollable()) { - this._cleanScrolling(); - } + _updateScrollable() { + if (this.getScrollable()) { + this._cleanScrolling(); + } - this._renderScrolling(); - }, + this._renderScrolling(); + }, - _renderScrollable() { - const $itemContainer = this.$element().wrapInner($('
').addClass(TABS_SCROLLABLE_CLASS)).children(); + _renderScrollable() { + const $itemContainer = this.$element().wrapInner($('
').addClass(TABS_SCROLLABLE_CLASS)).children(); - this._scrollable = this._createComponent($itemContainer, Scrollable, { - direction: this._getScrollableDirection(), - showScrollbar: 'never', - useKeyboard: false, - useNative: false, - scrollByContent: this.option('scrollByContent'), - onScroll: () => { - this._updateNavButtonsState(); - }, - }); + this._scrollable = this._createComponent($itemContainer, Scrollable, { + direction: this._getScrollableDirection(), + showScrollbar: 'never', + useKeyboard: false, + useNative: false, + scrollByContent: this.option('scrollByContent'), + onScroll: () => { + this._updateNavButtonsState(); + }, + }); - this.$element().append(this._scrollable.$element()); - }, + this.$element().append(this._scrollable.$element()); + }, - _scrollToItem: function(itemData) { - if(!this._scrollable) return; + _scrollToItem(itemData) { + if (!this._scrollable) return; - const $item = this._editStrategy.getItemElement(itemData); - this._scrollable.scrollToElement($item); - }, + const $item = this._editStrategy.getItemElement(itemData); + this._scrollable.scrollToElement($item); + }, - _renderNavButtons: function() { - const { showNavButtons, rtlEnabled } = this.option(); + _renderNavButtons() { + const { showNavButtons, rtlEnabled } = this.option(); - this.$element().toggleClass(TABS_NAV_BUTTONS_CLASS, showNavButtons); + this.$element().toggleClass(TABS_NAV_BUTTONS_CLASS, showNavButtons); - if(!showNavButtons) { - return; - } + if (!showNavButtons) { + return; + } - this._leftButton = this._createNavButton(-TAB_OFFSET, rtlEnabled ? BUTTON_NEXT_ICON : BUTTON_PREV_ICON); - const $leftButton = this._leftButton.$element(); + this._leftButton = this._createNavButton(-TAB_OFFSET, rtlEnabled ? BUTTON_NEXT_ICON : BUTTON_PREV_ICON); + const $leftButton = this._leftButton.$element(); - $leftButton.addClass(TABS_LEFT_NAV_BUTTON_CLASS); + $leftButton.addClass(TABS_LEFT_NAV_BUTTON_CLASS); - this.$element().prepend($leftButton); + this.$element().prepend($leftButton); - this._rightButton = this._createNavButton(TAB_OFFSET, rtlEnabled ? BUTTON_PREV_ICON : BUTTON_NEXT_ICON); - const $rightButton = this._rightButton.$element(); + this._rightButton = this._createNavButton(TAB_OFFSET, rtlEnabled ? BUTTON_PREV_ICON : BUTTON_NEXT_ICON); + const $rightButton = this._rightButton.$element(); - $rightButton.addClass(TABS_RIGHT_NAV_BUTTON_CLASS); + $rightButton.addClass(TABS_RIGHT_NAV_BUTTON_CLASS); - this.$element().append($rightButton); - }, + this.$element().append($rightButton); + }, - _updateNavButtonsAriaDisabled() { - const buttons = [this._leftButton, this._rightButton]; + _updateNavButtonsAriaDisabled() { + const buttons = [this._leftButton, this._rightButton]; - buttons.forEach(button => { - button?.$element().attr({ 'aria-disabled': null }); - }); - }, + buttons.forEach((button) => { + button?.$element().attr({ 'aria-disabled': null }); + }); + }, - _updateNavButtonsState() { - const isVertical = this._isVertical(); - const scrollable = this.getScrollable(); + _updateNavButtonsState() { + const isVertical = this._isVertical(); + const scrollable = this.getScrollable(); - if(isVertical) { - this._leftButton?.option('disabled', isReachedTop(scrollable.scrollTop(), 1)); - this._rightButton?.option('disabled', isReachedBottom($(scrollable.container()).get(0), scrollable.scrollTop(), 0, 1)); - } else { - this._leftButton?.option('disabled', isReachedLeft(scrollable.scrollLeft(), 1)); - this._rightButton?.option('disabled', isReachedRight($(scrollable.container()).get(0), scrollable.scrollLeft(), 1)); - } + if (isVertical) { + this._leftButton?.option('disabled', isReachedTop(scrollable.scrollTop(), 1)); + // @ts-expect-error + this._rightButton?.option('disabled', isReachedBottom($(scrollable.container()).get(0), scrollable.scrollTop(), 0, 1)); + } else { + this._leftButton?.option('disabled', isReachedLeft(scrollable.scrollLeft(), 1)); + // @ts-expect-error + this._rightButton?.option('disabled', isReachedRight($(scrollable.container()).get(0), scrollable.scrollLeft(), 1)); + } - this._updateNavButtonsAriaDisabled(); - }, - - _updateScrollPosition: function(offset, duration) { - this._scrollable.update(); - this._scrollable.scrollBy(offset / duration); - }, - - _createNavButton: function(offset, icon) { - const holdAction = this._createAction(() => { - this._holdInterval = setInterval(() => { - this._updateScrollPosition(offset, FEEDBACK_DURATION_INTERVAL); - }, FEEDBACK_DURATION_INTERVAL); - }); - - const holdEventName = addNamespace(holdEvent.name, 'dxNavButton'); - const pointerUpEventName = addNamespace(pointerEvents.up, 'dxNavButton'); - const pointerOutEventName = addNamespace(pointerEvents.out, 'dxNavButton'); - - const navButton = this._createComponent($('
').addClass(TABS_NAV_BUTTON_CLASS), Button, { - focusStateEnabled: false, - icon: icon, - integrationOptions: {}, - elementAttr: { - role: null, - 'aria-label': null, - 'aria-disabled': null, - }, - onClick: () => { - this._updateScrollPosition(offset, 1); - }, - }); - - const $navButton = navButton.$element(); - - eventsEngine.on($navButton, holdEventName, { timeout: FEEDBACK_SCROLL_TIMEOUT }, (e) => { - holdAction({ event: e }); - }); - eventsEngine.on($navButton, pointerUpEventName, () => { - this._clearInterval(); - }); - eventsEngine.on($navButton, pointerOutEventName, () => { - this._clearInterval(); - }); - - return navButton; - }, - - _clearInterval: function() { - if(this._holdInterval) clearInterval(this._holdInterval); - }, - - _updateSelection: function(addedSelection) { - this._scrollable && this._scrollable.scrollToElement(this.itemElements().eq(addedSelection[0])); - }, - - _visibilityChanged: function(visible) { - if(visible) { - this._dimensionChanged(); - } - }, + this._updateNavButtonsAriaDisabled(); + }, + + _updateScrollPosition(offset, duration) { + this._scrollable.update(); + this._scrollable.scrollBy(offset / duration); + }, + + _createNavButton(offset, icon) { + const holdAction = this._createAction(() => { + this._holdInterval = setInterval(() => { + this._updateScrollPosition(offset, FEEDBACK_DURATION_INTERVAL); + }, FEEDBACK_DURATION_INTERVAL); + }); + + const holdEventName = addNamespace(holdEvent.name, 'dxNavButton'); + const pointerUpEventName = addNamespace(pointerEvents.up, 'dxNavButton'); + const pointerOutEventName = addNamespace(pointerEvents.out, 'dxNavButton'); + + const navButton = this._createComponent($('
').addClass(TABS_NAV_BUTTON_CLASS), Button, { + focusStateEnabled: false, + icon, + integrationOptions: {}, + elementAttr: { + role: null, + 'aria-label': null, + 'aria-disabled': null, + }, + onClick: () => { + this._updateScrollPosition(offset, 1); + }, + }); + + const $navButton = navButton.$element(); + + eventsEngine.on($navButton, holdEventName, { timeout: FEEDBACK_SCROLL_TIMEOUT }, (e) => { + holdAction({ event: e }); + }); + eventsEngine.on($navButton, pointerUpEventName, () => { + this._clearInterval(); + }); + eventsEngine.on($navButton, pointerOutEventName, () => { + this._clearInterval(); + }); + + return navButton; + }, + + _clearInterval() { + if (this._holdInterval) clearInterval(this._holdInterval); + }, + + _updateSelection(addedSelection) { + this._scrollable && this._scrollable.scrollToElement(this.itemElements().eq(addedSelection[0])); + }, + + _visibilityChanged(visible) { + if (visible) { + this._dimensionChanged(); + } + }, - _dimensionChanged: function() { - this._renderScrolling(); - }, + _dimensionChanged() { + this._renderScrolling(); + }, - _itemSelectHandler: function(e) { - if(this.option('selectionMode') === 'single' && this.isItemSelected(e.currentTarget)) { - return; - } + _itemSelectHandler(e) { + if (this.option('selectionMode') === 'single' && this.isItemSelected(e.currentTarget)) { + return; + } - this.callBase(e); - }, + this.callBase(e); + }, - _clean: function() { - this._deferredTemplates = []; - this._cleanScrolling(); - this.callBase(); - }, + _clean() { + this._deferredTemplates = []; + this._cleanScrolling(); + this.callBase(); + }, - _toggleTabsVerticalClass(value) { - this.$element().toggleClass(TABS_ORIENTATION_CLASS.vertical, value); - }, + _toggleTabsVerticalClass(value) { + this.$element().toggleClass(TABS_ORIENTATION_CLASS.vertical, value); + }, - _toggleTabsHorizontalClass(value) { - this.$element().toggleClass(TABS_ORIENTATION_CLASS.horizontal, value); - }, + _toggleTabsHorizontalClass(value) { + this.$element().toggleClass(TABS_ORIENTATION_CLASS.horizontal, value); + }, - _getIndicatorPositionClass(indicatorPosition) { - return INDICATOR_POSITION_CLASS[indicatorPosition]; - }, + _getIndicatorPositionClass(indicatorPosition) { + return INDICATOR_POSITION_CLASS[indicatorPosition]; + }, - _getIndicatorPosition() { - const { _indicatorPosition, rtlEnabled } = this.option(); + _getIndicatorPosition() { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { _indicatorPosition, rtlEnabled } = this.option(); - if(_indicatorPosition) { - return _indicatorPosition; - } + if (_indicatorPosition) { + return _indicatorPosition; + } - const isVertical = this._isVertical(); + const isVertical = this._isVertical(); - if(rtlEnabled) { - return isVertical ? INDICATOR_POSITION.left : INDICATOR_POSITION.bottom; - } else { - return isVertical ? INDICATOR_POSITION.right : INDICATOR_POSITION.bottom; - } - }, - - _toggleIndicatorPositionClass(indicatorPosition) { - const newClass = this._getIndicatorPositionClass(indicatorPosition); - - this._toggleElementClasses(INDICATOR_POSITION_CLASS, newClass); - }, - - _toggleScrollingEnabledClass(scrollingEnabled) { - this.$element().toggleClass(TABS_SCROLLING_ENABLED_CLASS, Boolean(scrollingEnabled)); - }, - - _toggleOrientationClass(orientation) { - const isVertical = orientation === ORIENTATION.vertical; - - this._toggleTabsVerticalClass(isVertical); - this._toggleTabsHorizontalClass(!isVertical); - }, - - _getTabsIconPositionClass() { - const position = this.option('iconPosition'); - - switch(position) { - case ICON_POSITION.top: - return TABS_ICON_POSITION_CLASS.top; - case ICON_POSITION.end: - return TABS_ICON_POSITION_CLASS.end; - case ICON_POSITION.bottom: - return TABS_ICON_POSITION_CLASS.bottom; - case ICON_POSITION.start: - default: - return TABS_ICON_POSITION_CLASS.start; - } - }, + if (rtlEnabled) { + return isVertical ? INDICATOR_POSITION.left : INDICATOR_POSITION.bottom; + } + return isVertical ? INDICATOR_POSITION.right : INDICATOR_POSITION.bottom; + }, + + _toggleIndicatorPositionClass(indicatorPosition) { + const newClass = this._getIndicatorPositionClass(indicatorPosition); + + this._toggleElementClasses(INDICATOR_POSITION_CLASS, newClass); + }, + + _toggleScrollingEnabledClass(scrollingEnabled) { + this.$element().toggleClass(TABS_SCROLLING_ENABLED_CLASS, Boolean(scrollingEnabled)); + }, + + _toggleOrientationClass(orientation) { + const isVertical = orientation === ORIENTATION.vertical; + + this._toggleTabsVerticalClass(isVertical); + this._toggleTabsHorizontalClass(!isVertical); + }, + + _getTabsIconPositionClass() { + const position = this.option('iconPosition'); + + switch (position) { + case ICON_POSITION.top: + return TABS_ICON_POSITION_CLASS.top; + case ICON_POSITION.end: + return TABS_ICON_POSITION_CLASS.end; + case ICON_POSITION.bottom: + return TABS_ICON_POSITION_CLASS.bottom; + case ICON_POSITION.start: + default: + return TABS_ICON_POSITION_CLASS.start; + } + }, - _toggleIconPositionClass() { - const newClass = this._getTabsIconPositionClass(); + _toggleIconPositionClass() { + const newClass = this._getTabsIconPositionClass(); - this._toggleElementClasses(TABS_ICON_POSITION_CLASS, newClass); - }, + this._toggleElementClasses(TABS_ICON_POSITION_CLASS, newClass); + }, - _toggleStylingModeClass(value) { - const newClass = TABS_STYLING_MODE_CLASS[value] ?? TABS_STYLING_MODE_CLASS.primary; + _toggleStylingModeClass(value) { + const newClass = TABS_STYLING_MODE_CLASS[value] ?? TABS_STYLING_MODE_CLASS.primary; - this._toggleElementClasses(TABS_STYLING_MODE_CLASS, newClass); - }, + this._toggleElementClasses(TABS_STYLING_MODE_CLASS, newClass); + }, - _toggleElementClasses(classMap, newClass) { - for(const key in classMap) { - this.$element().removeClass(classMap[key]); - } + _toggleElementClasses(classMap, newClass) { + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const key in classMap) { + this.$element().removeClass(classMap[key]); + } - this.$element().addClass(newClass); - }, + this.$element().addClass(newClass); + }, - _toggleFocusedDisabledNextClass(currentIndex, isNextDisabled) { - this._itemElements().eq(currentIndex).toggleClass(FOCUSED_DISABLED_NEXT_TAB_CLASS, isNextDisabled); - }, + _toggleFocusedDisabledNextClass(currentIndex, isNextDisabled) { + this._itemElements().eq(currentIndex).toggleClass(FOCUSED_DISABLED_NEXT_TAB_CLASS, isNextDisabled); + }, - _toggleFocusedDisabledPrevClass(currentIndex, isPrevDisabled) { - this._itemElements().eq(currentIndex).toggleClass(FOCUSED_DISABLED_PREV_TAB_CLASS, isPrevDisabled); - }, + _toggleFocusedDisabledPrevClass(currentIndex, isPrevDisabled) { + this._itemElements().eq(currentIndex).toggleClass(FOCUSED_DISABLED_PREV_TAB_CLASS, isPrevDisabled); + }, - _toggleFocusedDisabledClasses(value) { - const { selectedIndex: currentIndex } = this.option(); + _toggleFocusedDisabledClasses(value) { + const { selectedIndex: currentIndex } = this.option(); - this._itemElements() - .removeClass(FOCUSED_DISABLED_NEXT_TAB_CLASS) - .removeClass(FOCUSED_DISABLED_PREV_TAB_CLASS); + this._itemElements() + .removeClass(FOCUSED_DISABLED_NEXT_TAB_CLASS) + .removeClass(FOCUSED_DISABLED_PREV_TAB_CLASS); - const prevItemIndex = currentIndex - 1; - const nextItemIndex = currentIndex + 1; + const prevItemIndex = currentIndex - 1; + const nextItemIndex = currentIndex + 1; - const nextFocusedIndex = $(value).index(); + const nextFocusedIndex = $(value).index(); - const isNextDisabled = this._itemElements().eq(nextItemIndex).hasClass(STATE_DISABLED_CLASS); - const isPrevDisabled = this._itemElements().eq(prevItemIndex).hasClass(STATE_DISABLED_CLASS); + const isNextDisabled = this._itemElements().eq(nextItemIndex).hasClass(STATE_DISABLED_CLASS); + const isPrevDisabled = this._itemElements().eq(prevItemIndex).hasClass(STATE_DISABLED_CLASS); - const shouldNextClassBeSetted = isNextDisabled && nextFocusedIndex === nextItemIndex; - const shouldPrevClassBeSetted = isPrevDisabled && nextFocusedIndex === prevItemIndex; + const shouldNextClassBeSetted = isNextDisabled && nextFocusedIndex === nextItemIndex; + const shouldPrevClassBeSetted = isPrevDisabled && nextFocusedIndex === prevItemIndex; - this._toggleFocusedDisabledNextClass(currentIndex, shouldNextClassBeSetted); - this._toggleFocusedDisabledPrevClass(currentIndex, shouldPrevClassBeSetted); - }, + this._toggleFocusedDisabledNextClass(currentIndex, shouldNextClassBeSetted); + this._toggleFocusedDisabledPrevClass(currentIndex, shouldPrevClassBeSetted); + }, - _updateFocusedElement() { - const { focusStateEnabled, selectedIndex } = this.option(); - const itemElements = this._itemElements(); + _updateFocusedElement() { + const { focusStateEnabled, selectedIndex } = this.option(); + const itemElements = this._itemElements(); - if(focusStateEnabled && itemElements.length) { - const selectedItem = itemElements.get(selectedIndex); + if (focusStateEnabled && itemElements.length) { + const selectedItem = itemElements.get(selectedIndex); - this.option({ focusedElement: selectedItem }); + this.option({ focusedElement: selectedItem }); + } + }, + + _optionChanged(args) { + switch (args.name) { + case 'useInkRipple': + case 'scrollingEnabled': + this._toggleScrollingEnabledClass(args.value); + this._invalidate(); + break; + case 'showNavButtons': + this._invalidate(); + break; + case 'scrollByContent': + this._scrollable && this._scrollable.option(args.name, args.value); + break; + case 'width': + case 'height': + this.callBase(args); + this._dimensionChanged(); + break; + case 'selectionMode': + this._renderMultiple(); + this.callBase(args); + break; + case 'badgeExpr': + this._invalidate(); + break; + case 'focusedElement': { + this._toggleFocusedDisabledClasses(args.value); + this.callBase(args); + this._scrollToItem(args.value); + break; + } + case 'rtlEnabled': { + this.callBase(args); + const indicatorPosition = this._getIndicatorPosition(); + this._toggleIndicatorPositionClass(indicatorPosition); + break; + } + case 'orientation': { + this._toggleOrientationClass(args.value); + const indicatorPosition = this._getIndicatorPosition(); + this._toggleIndicatorPositionClass(indicatorPosition); + if (hasWindow()) { + this._updateScrollable(); + } + break; + } + case 'iconPosition': { + this._toggleIconPositionClass(); + if (hasWindow()) { + this._dimensionChanged(); } - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'useInkRipple': - case 'scrollingEnabled': - this._toggleScrollingEnabledClass(args.value); - this._invalidate(); - break; - case 'showNavButtons': - this._invalidate(); - break; - case 'scrollByContent': - this._scrollable && this._scrollable.option(args.name, args.value); - break; - case 'width': - case 'height': - this.callBase(args); - this._dimensionChanged(); - break; - case 'selectionMode': - this._renderMultiple(); - this.callBase(args); - break; - case 'badgeExpr': - this._invalidate(); - break; - case 'focusedElement': { - this._toggleFocusedDisabledClasses(args.value); - this.callBase(args); - this._scrollToItem(args.value); - break; - } - case 'rtlEnabled': { - this.callBase(args); - const indicatorPosition = this._getIndicatorPosition(); - this._toggleIndicatorPositionClass(indicatorPosition); - break; - } - case 'orientation': { - this._toggleOrientationClass(args.value); - const indicatorPosition = this._getIndicatorPosition(); - this._toggleIndicatorPositionClass(indicatorPosition); - if(hasWindow()) { - this._updateScrollable(); - } - break; - } - case 'iconPosition': { - this._toggleIconPositionClass(); - if(hasWindow()) { - this._dimensionChanged(); - } - break; - } - case 'stylingMode': { - this._toggleStylingModeClass(args.value); - if(hasWindow()) { - this._dimensionChanged(); - } - break; - } - case '_indicatorPosition': { - this._toggleIndicatorPositionClass(args.value); - break; - } - case 'selectedIndex': - case 'selectedItem': - case 'selectedItems': - this.callBase(args); - this._updateFocusedElement(); - break; - default: - this.callBase(args); + break; + } + case 'stylingMode': { + this._toggleStylingModeClass(args.value); + if (hasWindow()) { + this._dimensionChanged(); } - }, + break; + } + case '_indicatorPosition': { + this._toggleIndicatorPositionClass(args.value); + break; + } + case 'selectedIndex': + case 'selectedItem': + case 'selectedItems': + this.callBase(args); + this._updateFocusedElement(); + break; + default: + this.callBase(args); + } + }, - _afterItemElementInserted() { - this.callBase(); - this._deferRenderScrolling(); - }, + _afterItemElementInserted() { + this.callBase(); + this._deferRenderScrolling(); + }, - _afterItemElementDeleted($item, deletedActionArgs) { - this.callBase($item, deletedActionArgs); - this._renderScrolling(); - }, + _afterItemElementDeleted($item, deletedActionArgs) { + this.callBase($item, deletedActionArgs); + this._renderScrolling(); + }, - getScrollable() { - return this._scrollable; - } + getScrollable() { + return this._scrollable; + }, }); Tabs.ItemClass = TabsItem; @@ -817,9 +817,3 @@ Tabs.ItemClass = TabsItem; registerComponent('dxTabs', Tabs); export default Tabs; - -/** - * @name dxTabsItem - * @inherits CollectionWidgetItem - * @type object - */ diff --git a/packages/devextreme/js/ui/tab_panel.js b/packages/devextreme/js/ui/tab_panel.js new file mode 100644 index 000000000000..6ef194b94427 --- /dev/null +++ b/packages/devextreme/js/ui/tab_panel.js @@ -0,0 +1,16 @@ +import TabPanel from '../__internal/ui/tab_panel/m_tab_panel'; + +export default TabPanel; + +// STYLE tabPanel + +/** + * @name dxTabPanelItem.visible + * @hidden + */ + +/** + * @name dxTabPanelItem + * @inherits dxMultiViewItem + * @type object + */ diff --git a/packages/devextreme/js/ui/tabs.js b/packages/devextreme/js/ui/tabs.js new file mode 100644 index 000000000000..f556b93ceb19 --- /dev/null +++ b/packages/devextreme/js/ui/tabs.js @@ -0,0 +1,17 @@ +import Tabs from '../__internal/ui/tabs/m_tabs'; + +export default Tabs; + +// STYLE tabs + +/** + * @name dxTabsOptions.activeStateEnabled + * @hidden + * @default true + */ + +/** + * @name dxTabsItem + * @inherits CollectionWidgetItem + * @type object + */ From 95cfd8939b3c5b2a4e68387fdbfdfb9848f23de3 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Fri, 7 Jun 2024 01:04:53 +0400 Subject: [PATCH 06/32] Overlay: move files to TS --- .../overlay/ui.overlay.js => __internal/ui/overlay/m_overlay.ts} | 0 .../ui/overlay/m_overlay_position_controller.ts} | 0 .../js/{ui/overlay/utils.js => __internal/ui/overlay/m_utils.ts} | 0 .../{ui/overlay/z_index.js => __internal/ui/overlay/m_z_index.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/overlay/ui.overlay.js => __internal/ui/overlay/m_overlay.ts} (100%) rename packages/devextreme/js/{ui/overlay/overlay_position_controller.js => __internal/ui/overlay/m_overlay_position_controller.ts} (100%) rename packages/devextreme/js/{ui/overlay/utils.js => __internal/ui/overlay/m_utils.ts} (100%) rename packages/devextreme/js/{ui/overlay/z_index.js => __internal/ui/overlay/m_z_index.ts} (100%) diff --git a/packages/devextreme/js/ui/overlay/ui.overlay.js b/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts similarity index 100% rename from packages/devextreme/js/ui/overlay/ui.overlay.js rename to packages/devextreme/js/__internal/ui/overlay/m_overlay.ts diff --git a/packages/devextreme/js/ui/overlay/overlay_position_controller.js b/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts similarity index 100% rename from packages/devextreme/js/ui/overlay/overlay_position_controller.js rename to packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts diff --git a/packages/devextreme/js/ui/overlay/utils.js b/packages/devextreme/js/__internal/ui/overlay/m_utils.ts similarity index 100% rename from packages/devextreme/js/ui/overlay/utils.js rename to packages/devextreme/js/__internal/ui/overlay/m_utils.ts diff --git a/packages/devextreme/js/ui/overlay/z_index.js b/packages/devextreme/js/__internal/ui/overlay/m_z_index.ts similarity index 100% rename from packages/devextreme/js/ui/overlay/z_index.js rename to packages/devextreme/js/__internal/ui/overlay/m_z_index.ts From bc127f17d3427abc42ee43fd6724add2203d4938 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 10 Jun 2024 13:46:04 +0400 Subject: [PATCH 07/32] Overlay: ignore errors after move to TS --- .../core/license/license_validation.test.ts | 2 +- .../filter_builder/m_filter_builder.ts | 2 +- .../js/__internal/ui/m_drop_down_box.ts | 2 +- .../js/__internal/ui/menu/m_menu.ts | 3 +- .../js/__internal/ui/overlay/m_overlay.ts | 2169 +++++++++-------- .../overlay/m_overlay_position_controller.ts | 394 +-- .../js/__internal/ui/overlay/m_utils.ts | 42 +- .../js/__internal/ui/overlay/m_z_index.ts | 32 +- .../m_slider_tooltip_position_controller.ts | 1 - .../devextreme/js/ui/diagram/ui.diagram.js | 2 +- .../devextreme/js/ui/overlay/ui.overlay.js | 13 + .../ui/popover/popover_position_controller.js | 2 +- .../js/ui/popup/popup_position_controller.js | 2 +- packages/devextreme/js/ui/popup/ui.popup.js | 2 +- .../drawer.scenarios.tests.js | 2 +- .../DevExpress.ui.widgets/overlay.tests.js | 2 +- 16 files changed, 1360 insertions(+), 1312 deletions(-) create mode 100644 packages/devextreme/js/ui/overlay/ui.overlay.js diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index 81ee460c559d..1c5bd8853347 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -1,6 +1,6 @@ import errors from '@js/core/errors'; -import { base } from '../../../ui/overlay/z_index'; +import { base } from '../../ui/overlay/m_z_index'; import { assertDevExtremeVersion, clearAssertedVersions } from '../../utils/version'; import { parseLicenseKey, diff --git a/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts b/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts index c4e07cb8786d..9fd5b80d4048 100644 --- a/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts +++ b/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts @@ -8,11 +8,11 @@ import { isDefined } from '@js/core/utils/type'; import eventsEngine from '@js/events/core/events_engine'; import { normalizeKeyName } from '@js/events/utils/index'; import messageLocalization from '@js/localization/message'; -import { getElementMaxHeightByWindow } from '@js/ui/overlay/utils'; import Popup from '@js/ui/popup'; import EditorFactoryMixin from '@js/ui/shared/ui.editor_factory_mixin'; import TreeView from '@js/ui/tree_view'; import Widget from '@js/ui/widget/ui.widget'; +import { getElementMaxHeightByWindow } from '@ts/ui/overlay/m_utils'; import { addItem, convertToInnerStructure, diff --git a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts index 9a0e87e30391..fa7f5c0d77e7 100644 --- a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts +++ b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts @@ -13,8 +13,8 @@ import eventsEngine from '@js/events/core/events_engine'; import { normalizeKeyName } from '@js/events/utils/index'; import DropDownEditor from '@js/ui/drop_down_editor/ui.drop_down_editor'; import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; -import { getElementMaxHeightByWindow } from '@js/ui/overlay/utils'; import { tabbable } from '@js/ui/widget/selectors'; +import { getElementMaxHeightByWindow } from '@ts/ui/overlay/m_utils'; const { getActiveElement } = domAdapter; diff --git a/packages/devextreme/js/__internal/ui/menu/m_menu.ts b/packages/devextreme/js/__internal/ui/menu/m_menu.ts index fe8114b2ea5e..0305fbf68e34 100644 --- a/packages/devextreme/js/__internal/ui/menu/m_menu.ts +++ b/packages/devextreme/js/__internal/ui/menu/m_menu.ts @@ -17,9 +17,9 @@ import type { Item, Properties } from '@js/ui/menu'; import type { Properties as OverlayProperties } from '@js/ui/overlay'; import type dxOverlay from '@js/ui/overlay'; import Overlay from '@js/ui/overlay/ui.overlay'; -import { getElementMaxHeightByWindow } from '@js/ui/overlay/utils'; import TreeView from '@js/ui/tree_view'; import MenuBase from '@ts/ui/context_menu/m_menu_base'; +import { getElementMaxHeightByWindow } from '@ts/ui/overlay/m_utils'; import Submenu from './m_submenu'; @@ -383,6 +383,7 @@ class Menu extends MenuBase { return { _ignoreFunctionValueDeprecation: true, + // @ts-expect-error maxHeight: () => getElementMaxHeightByWindow(this.$element()), deferRendering: false, shading: false, diff --git a/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts b/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts index caa6ae7a74ee..57bd56c97295 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts +++ b/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts @@ -1,36 +1,39 @@ -import { getOuterWidth, getOuterHeight } from '../../core/utils/size'; -import fx from '../../animation/fx'; -import registerComponent from '../../core/component_registrator'; -import devices from '../../core/devices'; -import domAdapter from '../../core/dom_adapter'; -import { getPublicElement } from '../../core/element'; -import $ from '../../core/renderer'; -import { EmptyTemplate } from '../../core/templates/empty_template'; -import { noop } from '../../core/utils/common'; -import { Deferred } from '../../core/utils/deferred'; -import { contains, resetActiveElement } from '../../core/utils/dom'; -import { extend } from '../../core/utils/extend'; -import { each } from '../../core/utils/iterator'; -import readyCallbacks from '../../core/utils/ready_callbacks'; -import { isFunction, isObject, isPromise, isWindow } from '../../core/utils/type'; -import { changeCallback } from '../../core/utils/view_port'; -import { getWindow, hasWindow } from '../../core/utils/window'; -import errors from '../../core/errors'; -import uiErrors from '../widget/ui.errors'; -import eventsEngine from '../../events/core/events_engine'; +import fx from '@js/animation/fx'; +import registerComponent from '@js/core/component_registrator'; +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import { getPublicElement } from '@js/core/element'; +import errors from '@js/core/errors'; +import $ from '@js/core/renderer'; +import { EmptyTemplate } from '@js/core/templates/empty_template'; +import browser from '@js/core/utils/browser'; +import { noop } from '@js/core/utils/common'; +import { Deferred } from '@js/core/utils/deferred'; +import { contains, resetActiveElement } from '@js/core/utils/dom'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import readyCallbacks from '@js/core/utils/ready_callbacks'; +import { getOuterHeight, getOuterWidth } from '@js/core/utils/size'; import { - move as dragEventMove -} from '../../events/drag'; -import pointerEvents from '../../events/pointer'; -import { keyboard } from '../../events/short'; -import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '../../events/utils/index'; -import { triggerHidingEvent, triggerResizeEvent, triggerShownEvent } from '../../events/visibility_change'; -import { hideCallback as hideTopOverlayCallback } from '../../mobile/hide_callback'; -import { tabbable } from '../widget/selectors'; -import Widget from '../widget/ui.widget'; -import browser from '../../core/utils/browser'; -import * as zIndexPool from './z_index'; -import { OverlayPositionController, OVERLAY_POSITION_ALIASES } from './overlay_position_controller'; + isFunction, isObject, isPromise, isWindow, +} from '@js/core/utils/type'; +import { changeCallback } from '@js/core/utils/view_port'; +import { getWindow, hasWindow } from '@js/core/utils/window'; +import eventsEngine from '@js/events/core/events_engine'; +import { + move as dragEventMove, +} from '@js/events/drag'; +import pointerEvents from '@js/events/pointer'; +import { keyboard } from '@js/events/short'; +import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '@js/events/utils/index'; +import { triggerHidingEvent, triggerResizeEvent, triggerShownEvent } from '@js/events/visibility_change'; +import { hideCallback as hideTopOverlayCallback } from '@js/mobile/hide_callback'; +import { tabbable } from '@js/ui/widget/selectors'; +import uiErrors from '@js/ui/widget/ui.errors'; +import Widget from '@js/ui/widget/ui.widget'; + +import { OVERLAY_POSITION_ALIASES, OverlayPositionController } from './m_overlay_position_controller'; +import * as zIndexPool from './m_z_index'; const ready = readyCallbacks.add; const window = getWindow(); @@ -54,1242 +57,1240 @@ const PREVENT_SAFARI_SCROLLING_CLASS = 'dx-prevent-safari-scrolling'; const TAB_KEY = 'tab'; ready(() => { - eventsEngine.subscribeGlobal(domAdapter.getDocument(), pointerEvents.down, e => { - for(let i = OVERLAY_STACK.length - 1; i >= 0; i--) { - if(!OVERLAY_STACK[i]._proxiedDocumentDownHandler(e)) { - return; - } - } - }); + // @ts-expect-error + eventsEngine.subscribeGlobal(domAdapter.getDocument(), pointerEvents.down, (e) => { + for (let i = OVERLAY_STACK.length - 1; i >= 0; i--) { + // @ts-expect-error + if (!OVERLAY_STACK[i]._proxiedDocumentDownHandler(e)) { + return; + } + } + }); }); - +// @ts-expect-error const Overlay = Widget.inherit({ - _supportedKeys: function() { - return extend(this.callBase(), { - escape: function() { - this.hide(); - }, - }); - }, - - _getDefaultOptions: function() { - return extend(this.callBase(), { - /** - * @name dxOverlayOptions.activeStateEnabled - * @hidden - */ - activeStateEnabled: false, - - visible: false, + _supportedKeys() { + return extend(this.callBase(), { + escape() { + this.hide(); + }, + }); + }, - deferRendering: true, + _getDefaultOptions() { + return extend(this.callBase(), { + activeStateEnabled: false, - shading: true, + visible: false, - shadingColor: '', + deferRendering: true, + shading: true, - wrapperAttr: {}, + shadingColor: '', - position: extend({}, OVERLAY_POSITION_ALIASES.center), + wrapperAttr: {}, - width: '80vw', + position: extend({}, OVERLAY_POSITION_ALIASES.center), - minWidth: null, + width: '80vw', - maxWidth: null, + minWidth: null, - height: '80vh', + maxWidth: null, - minHeight: null, + height: '80vh', - maxHeight: null, + minHeight: null, - animation: { - show: { - type: 'pop', - duration: 300, - from: { - scale: 0.55 - } - }, - hide: { - type: 'pop', - duration: 300, - from: { - opacity: 1, - scale: 1 - }, - to: { - opacity: 0, - scale: 0.55 - } - } - }, + maxHeight: null, - closeOnOutsideClick: false, - hideOnOutsideClick: false, + animation: { + show: { + type: 'pop', + duration: 300, + from: { + scale: 0.55, + }, + }, + hide: { + type: 'pop', + duration: 300, + from: { + opacity: 1, + scale: 1, + }, + to: { + opacity: 0, + scale: 0.55, + }, + }, + }, - _ignorePreventScrollEventsDeprecation: false, + closeOnOutsideClick: false, + hideOnOutsideClick: false, - onShowing: null, + _ignorePreventScrollEventsDeprecation: false, - onShown: null, + onShowing: null, - onHiding: null, + onShown: null, - onHidden: null, + onHiding: null, - contentTemplate: 'content', + onHidden: null, - innerOverlay: false, + contentTemplate: 'content', - restorePosition: true, + innerOverlay: false, - container: undefined, + restorePosition: true, - visualContainer: undefined, + container: undefined, - // NOTE: private options - hideTopOverlayHandler: () => { this.hide(); }, - hideOnParentScroll: false, + visualContainer: undefined, - preventScrollEvents: true, + // NOTE: private options + hideTopOverlayHandler: () => { this.hide(); }, + hideOnParentScroll: false, - onPositioned: null, - propagateOutsideClick: false, - ignoreChildEvents: true, - _checkParentVisibility: true, - _hideOnParentScrollTarget: undefined, - _fixWrapperPosition: false - }); - }, - - _defaultOptionsRules: function() { - return this.callBase().concat([{ - device: function() { - return !hasWindow(); - }, - options: { - width: null, - height: null, - animation: null, - _checkParentVisibility: false - } - }]); - }, - - _setOptionsByReference: function() { - this.callBase(); - - extend(this._optionsByReference, { - animation: true - }); - }, + preventScrollEvents: true, - $wrapper: function() { - return this._$wrapper; - }, + onPositioned: null, + propagateOutsideClick: false, + ignoreChildEvents: true, + _checkParentVisibility: true, + _hideOnParentScrollTarget: undefined, + _fixWrapperPosition: false, + }); + }, + + _defaultOptionsRules() { + return this.callBase().concat([{ + device() { + return !hasWindow(); + }, + options: { + width: null, + height: null, + animation: null, + _checkParentVisibility: false, + }, + }]); + }, + + _setOptionsByReference() { + this.callBase(); + + extend(this._optionsByReference, { + animation: true, + }); + }, - _eventBindingTarget: function() { - return this._$content; - }, + $wrapper() { + return this._$wrapper; + }, - _setDeprecatedOptions() { - this.callBase(); - extend(this._deprecatedOptions, { - 'closeOnOutsideClick': { since: '22.1', alias: 'hideOnOutsideClick' } - }); - }, + _eventBindingTarget() { + return this._$content; + }, - ctor: function(element, options) { - this.callBase(element, options); + _setDeprecatedOptions() { + this.callBase(); + extend(this._deprecatedOptions, { + closeOnOutsideClick: { since: '22.1', alias: 'hideOnOutsideClick' }, + }); + }, - if(options) { - if('preventScrollEvents' in options && !options._ignorePreventScrollEventsDeprecation) { - this._logDeprecatedPreventScrollEventsInfo(); - } - } - }, + ctor(element, options) { + this.callBase(element, options); - _logDeprecatedPreventScrollEventsInfo() { - this._logDeprecatedOptionWarning('preventScrollEvents', { - since: '23.1', - message: 'If you enable this option, end-users may experience scrolling issues.' - }); - }, + if (options) { + if ('preventScrollEvents' in options && !options._ignorePreventScrollEventsDeprecation) { + this._logDeprecatedPreventScrollEventsInfo(); + } + } + }, - _init: function() { - this.callBase(); - this._initActions(); - this._initHideOnOutsideClickHandler(); - this._initTabTerminatorHandler(); + _logDeprecatedPreventScrollEventsInfo() { + this._logDeprecatedOptionWarning('preventScrollEvents', { + since: '23.1', + message: 'If you enable this option, end-users may experience scrolling issues.', + }); + }, - this._customWrapperClass = null; - this._$wrapper = $('
').addClass(OVERLAY_WRAPPER_CLASS); - this._$content = $('
').addClass(OVERLAY_CONTENT_CLASS); - this._initInnerOverlayClass(); + _init() { + this.callBase(); + this._initActions(); + this._initHideOnOutsideClickHandler(); + this._initTabTerminatorHandler(); - const $element = this.$element(); - $element.addClass(OVERLAY_CLASS); + this._customWrapperClass = null; + this._$wrapper = $('
').addClass(OVERLAY_WRAPPER_CLASS); + this._$content = $('
').addClass(OVERLAY_CONTENT_CLASS); + this._initInnerOverlayClass(); - this._$wrapper.attr('data-bind', 'dxControlsDescendantBindings: true'); + const $element = this.$element(); + $element.addClass(OVERLAY_CLASS); - this._toggleViewPortSubscription(true); - this._initHideTopOverlayHandler(this.option('hideTopOverlayHandler')); - this._parentsScrollSubscriptionInfo = { - handler: e => { this._hideOnParentsScrollHandler(e); } - }; + this._$wrapper.attr('data-bind', 'dxControlsDescendantBindings: true'); - this.warnPositionAsFunction(); - }, + this._toggleViewPortSubscription(true); + this._initHideTopOverlayHandler(this.option('hideTopOverlayHandler')); + this._parentsScrollSubscriptionInfo = { + handler: (e) => { this._hideOnParentsScrollHandler(e); }, + }; - warnPositionAsFunction() { - if(isFunction(this.option('position'))) { // position as function deprecated in 21.2 - errors.log('W0018'); - } - }, + this.warnPositionAsFunction(); + }, - _initInnerOverlayClass: function() { - this._$content.toggleClass(INNER_OVERLAY_CLASS, this.option('innerOverlay')); - }, + warnPositionAsFunction() { + if (isFunction(this.option('position'))) { // position as function deprecated in 21.2 + errors.log('W0018'); + } + }, - _initHideTopOverlayHandler: function(handler) { - this._hideTopOverlayHandler = handler; - }, + _initInnerOverlayClass() { + this._$content.toggleClass(INNER_OVERLAY_CLASS, this.option('innerOverlay')); + }, - _getActionsList: function() { - return ['onShowing', 'onShown', 'onHiding', 'onHidden', 'onPositioned', 'onVisualPositionChanged']; - }, + _initHideTopOverlayHandler(handler) { + this._hideTopOverlayHandler = handler; + }, - _initActions: function() { - this._actions = {}; - const actions = this._getActionsList(); + _getActionsList() { + return ['onShowing', 'onShown', 'onHiding', 'onHidden', 'onPositioned', 'onVisualPositionChanged']; + }, - each(actions, (_, action) => { - this._actions[action] = this._createActionByOption(action, { - excludeValidators: ['disabled', 'readOnly'] - }) || noop; - }); - }, + _initActions() { + this._actions = {}; + const actions = this._getActionsList(); - _initHideOnOutsideClickHandler: function() { - this._proxiedDocumentDownHandler = (...args) => { - return this._documentDownHandler(...args); - }; - }, + each(actions, (_, action) => { + this._actions[action] = this._createActionByOption(action, { + excludeValidators: ['disabled', 'readOnly'], + }) || noop; + }); + }, - _initMarkup() { - this.callBase(); - this._renderWrapperAttributes(); - this._initPositionController(); - }, + _initHideOnOutsideClickHandler() { + this._proxiedDocumentDownHandler = (...args) => this._documentDownHandler(...args); + }, - _documentDownHandler: function(e) { - if(this._showAnimationProcessing) { - this._stopAnimation(); - } + _initMarkup() { + this.callBase(); + this._renderWrapperAttributes(); + this._initPositionController(); + }, - const isAttachedTarget = $(window.document).is(e.target) || contains(window.document, e.target); - const isInnerOverlay = $(e.target).closest(`.${INNER_OVERLAY_CLASS}`).length; - const outsideClick = isAttachedTarget && !isInnerOverlay && !(this._$content.is(e.target) + _documentDownHandler(e) { + if (this._showAnimationProcessing) { + this._stopAnimation(); + } + // @ts-expect-error + const isAttachedTarget = $(window.document).is(e.target) || contains(window.document, e.target); + const isInnerOverlay = $(e.target).closest(`.${INNER_OVERLAY_CLASS}`).length; + const outsideClick = isAttachedTarget && !isInnerOverlay && !(this._$content.is(e.target) || contains(this._$content.get(0), e.target)); - if(outsideClick && this._shouldHideOnOutsideClick(e)) { - this._outsideClickHandler(e); - } + if (outsideClick && this._shouldHideOnOutsideClick(e)) { + this._outsideClickHandler(e); + } - return this.option('propagateOutsideClick'); - }, + return this.option('propagateOutsideClick'); + }, - _shouldHideOnOutsideClick: function(e) { - const { hideOnOutsideClick } = this.option(); + _shouldHideOnOutsideClick(e) { + const { hideOnOutsideClick } = this.option(); - if(isFunction(hideOnOutsideClick)) { - return hideOnOutsideClick(e); - } + if (isFunction(hideOnOutsideClick)) { + return hideOnOutsideClick(e); + } - return hideOnOutsideClick; - }, + return hideOnOutsideClick; + }, - _outsideClickHandler(e) { - if(this.option('shading')) { - e.preventDefault(); - } + _outsideClickHandler(e) { + if (this.option('shading')) { + e.preventDefault(); + } - this.hide(); - }, + this.hide(); + }, - _getAnonymousTemplateName: function() { - return ANONYMOUS_TEMPLATE_NAME; - }, + _getAnonymousTemplateName() { + return ANONYMOUS_TEMPLATE_NAME; + }, - _initTemplates: function() { - this._templateManager.addDefaultTemplates({ - content: new EmptyTemplate() - }); - this.callBase(); - }, + _initTemplates() { + this._templateManager.addDefaultTemplates({ + content: new EmptyTemplate(), + }); + this.callBase(); + }, - _isTopOverlay: function() { - const overlayStack = this._overlayStack(); + _isTopOverlay() { + const overlayStack = this._overlayStack(); - for(let i = overlayStack.length - 1; i >= 0; i--) { - const tabbableElements = overlayStack[i]._findTabbableBounds(); + for (let i = overlayStack.length - 1; i >= 0; i--) { + const tabbableElements = overlayStack[i]._findTabbableBounds(); - if(tabbableElements.first || tabbableElements.last) { - return overlayStack[i] === this; - } - } + if (tabbableElements.first || tabbableElements.last) { + return overlayStack[i] === this; + } + } - return false; - }, + return false; + }, - _overlayStack: function() { - return OVERLAY_STACK; - }, + _overlayStack() { + return OVERLAY_STACK; + }, - _zIndexInitValue: function() { - return Overlay.baseZIndex(); - }, + _zIndexInitValue() { + return Overlay.baseZIndex(); + }, - _toggleViewPortSubscription: function(toggle) { - viewPortChanged.remove(this._viewPortChangeHandle); + _toggleViewPortSubscription(toggle) { + viewPortChanged.remove(this._viewPortChangeHandle); - if(toggle) { - this._viewPortChangeHandle = (...args) => { this._viewPortChangeHandler(...args); }; - viewPortChanged.add(this._viewPortChangeHandle); - } - }, + if (toggle) { + this._viewPortChangeHandle = (...args) => { this._viewPortChangeHandler(...args); }; + viewPortChanged.add(this._viewPortChangeHandle); + } + }, - _viewPortChangeHandler: function() { - this._positionController.updateContainer(this.option('container')); - this._refresh(); - }, + _viewPortChangeHandler() { + this._positionController.updateContainer(this.option('container')); + this._refresh(); + }, - _renderWrapperAttributes() { - const { wrapperAttr } = this.option(); - const attributes = extend({}, wrapperAttr); - const classNames = attributes.class; + _renderWrapperAttributes() { + const { wrapperAttr } = this.option(); + const attributes = extend({}, wrapperAttr); + const classNames = attributes.class; - delete attributes.class; + delete attributes.class; - this.$wrapper() - .attr(attributes) - .removeClass(this._customWrapperClass) - .addClass(classNames); + this.$wrapper() + .attr(attributes) + .removeClass(this._customWrapperClass) + .addClass(classNames); - this._customWrapperClass = classNames; - }, + this._customWrapperClass = classNames; + }, - _renderVisibilityAnimate: function(visible) { - this._stopAnimation(); + _renderVisibilityAnimate(visible) { + this._stopAnimation(); - return visible ? this._show() : this._hide(); - }, - - _getAnimationConfig: function() { - return this._getOptionValue('animation', this); - }, - - _toggleBodyScroll: noop, - - _animateShowing: function() { - const animation = this._getAnimationConfig() ?? {}; - const showAnimation = this._normalizeAnimation(animation.show, 'to'); - const startShowAnimation = showAnimation?.start ?? noop; - const completeShowAnimation = showAnimation?.complete ?? noop; - - this._animate( - showAnimation, - (...args) => { - if(this._isAnimationPaused) { - return; - } - if(this.option('focusStateEnabled')) { - eventsEngine.trigger(this._focusTarget(), 'focus'); - } - - completeShowAnimation.call(this, ...args); - this._showAnimationProcessing = false; - this._isHidden = false; - this._actions.onShown(); - this._toggleSafariScrolling(); - this._showingDeferred.resolve(); - }, - (...args) => { - if(this._isAnimationPaused) { - return; - } - startShowAnimation.call(this, ...args); - this._showAnimationProcessing = true; - }); - }, - - _processShowingHidingCancel: function(cancelArg, applyFunction, cancelFunction) { - if(isPromise(cancelArg)) { - cancelArg - .then(shouldCancel => { - if(shouldCancel) { - cancelFunction(); - } else { - applyFunction(); - } - }) - .catch(() => applyFunction()); - } else { - cancelArg ? cancelFunction() : applyFunction(); - } - }, + return visible ? this._show() : this._hide(); + }, - _show: function() { - this._showingDeferred = new Deferred(); + _getAnimationConfig() { + return this._getOptionValue('animation', this); + }, - this._parentHidden = this._isParentHidden(); - this._showingDeferred.done(() => { - delete this._parentHidden; - }); + _toggleBodyScroll: noop, - if(this._parentHidden) { - this._isHidden = true; - return this._showingDeferred.resolve(); - } + _animateShowing() { + const animation = this._getAnimationConfig() ?? {}; + const showAnimation = this._normalizeAnimation(animation.show, 'to'); + const startShowAnimation = showAnimation?.start ?? noop; + const completeShowAnimation = showAnimation?.complete ?? noop; - if(this._currentVisible) { - return new Deferred().resolve().promise(); + this._animate( + showAnimation, + (...args) => { + if (this._isAnimationPaused) { + return; } - this._currentVisible = true; - - if(this._isHidingActionCanceled) { - delete this._isHidingActionCanceled; - this._showingDeferred.reject(); - } else { - const show = () => { - this._stopAnimation(); - this._toggleBodyScroll(this.option('enableBodyScroll')); - this._toggleVisibility(true); - this._$content.css('visibility', 'hidden'); - this._$content.toggleClass(INVISIBLE_STATE_CLASS, false); - this._updateZIndexStackPosition(true); - this._positionController.openingHandled(); - this._renderContent(); - - const showingArgs = { cancel: false }; - this._actions.onShowing(showingArgs); - - const cancelShow = () => { - this._toggleVisibility(false); - this._$content.css('visibility', ''); - this._$content.toggleClass(INVISIBLE_STATE_CLASS, true); - this._isShowingActionCanceled = true; - this._moveFromContainer(); - this._toggleBodyScroll(true); - this.option('visible', false); - this._showingDeferred.resolve(); - }; - - const applyShow = () => { - this._$content.css('visibility', ''); - this._renderVisibility(true); - this._animateShowing(); - }; - - this._processShowingHidingCancel(showingArgs.cancel, applyShow, cancelShow); - }; - - if(this.option('templatesRenderAsynchronously')) { - this._stopShowTimer(); - // NOTE: T390360, T386038 - this._asyncShowTimeout = setTimeout(show); - } else { - show(); - } + if (this.option('focusStateEnabled')) { + // @ts-expect-error + eventsEngine.trigger(this._focusTarget(), 'focus'); } - return this._showingDeferred.promise(); - }, - - _normalizeAnimation: function(showHideConfig, direction) { - if(showHideConfig) { - showHideConfig = extend({ - type: 'slide', - skipElementInitialStyles: true, // NOTE: for fadeIn animation - }, showHideConfig); - - if(isObject(showHideConfig[direction])) { - extend(showHideConfig[direction], { - position: this._positionController.position - }); - } + completeShowAnimation.call(this, ...args); + this._showAnimationProcessing = false; + this._isHidden = false; + this._actions.onShown(); + this._toggleSafariScrolling(); + this._showingDeferred.resolve(); + }, + (...args) => { + if (this._isAnimationPaused) { + return; } + startShowAnimation.call(this, ...args); + this._showAnimationProcessing = true; + }, + ); + }, + + _processShowingHidingCancel(cancelArg, applyFunction, cancelFunction) { + if (isPromise(cancelArg)) { + cancelArg + .then((shouldCancel) => { + if (shouldCancel) { + cancelFunction(); + } else { + applyFunction(); + } + }) + .catch(() => applyFunction()); + } else { + cancelArg ? cancelFunction() : applyFunction(); + } + }, - return showHideConfig; - }, - - _animateHiding: function() { - const animation = this._getAnimationConfig() ?? {}; - const hideAnimation = this._normalizeAnimation(animation.hide, 'from'); - const startHideAnimation = hideAnimation?.start ?? noop; - const completeHideAnimation = hideAnimation?.complete ?? noop; - - this._animate( - hideAnimation, - (...args) => { - this._$content.css('pointerEvents', ''); - this._renderVisibility(false); - - completeHideAnimation.call(this, ...args); - this._hideAnimationProcessing = false; - this._actions?.onHidden(); - - this._hidingDeferred.resolve(); - }, - (...args) => { - this._$content.css('pointerEvents', 'none'); - startHideAnimation.call(this, ...args); - this._hideAnimationProcessing = true; - } - ); - }, - - _hide: function() { - if(!this._currentVisible) { - return new Deferred().resolve().promise(); - } - this._currentVisible = false; - - this._hidingDeferred = new Deferred(); - const hidingArgs = { cancel: false }; - - if(this._isShowingActionCanceled) { - delete this._isShowingActionCanceled; - this._hidingDeferred.reject(); - } else { - this._actions.onHiding(hidingArgs); - - this._toggleSafariScrolling(); - this._toggleBodyScroll(true); - - const cancelHide = () => { - this._isHidingActionCanceled = true; - this._toggleBodyScroll(this.option('enableBodyScroll')); - this.option('visible', true); - this._hidingDeferred.resolve(); - }; - - const applyHide = () => { - this._forceFocusLost(); - this._toggleShading(false); - this._toggleSubscriptions(false); - this._stopShowTimer(); - this._animateHiding(); - }; - - this._processShowingHidingCancel(hidingArgs.cancel, applyHide, cancelHide); - } - return this._hidingDeferred.promise(); - }, + _show() { + this._showingDeferred = Deferred(); - _forceFocusLost: function() { - const activeElement = domAdapter.getActiveElement(); - const shouldResetActiveElement = !!this._$content.find(activeElement).length; + this._parentHidden = this._isParentHidden(); + this._showingDeferred.done(() => { + delete this._parentHidden; + }); - if(shouldResetActiveElement) { - resetActiveElement(); - } - }, - - _animate: function(animation, completeCallback, startCallback) { - if(animation) { - startCallback = startCallback || animation.start || noop; - - fx.animate(this._$content, extend({}, animation, { - start: startCallback, - complete: completeCallback - })); - } else { - completeCallback(); - } - }, + if (this._parentHidden) { + this._isHidden = true; + return this._showingDeferred.resolve(); + } - _stopAnimation: function() { - fx.stop(this._$content, true); - }, + if (this._currentVisible) { + return Deferred().resolve().promise(); + } + this._currentVisible = true; - _renderVisibility: function(visible) { - if(visible && this._isParentHidden()) { - return; - } + if (this._isHidingActionCanceled) { + delete this._isHidingActionCanceled; + this._showingDeferred.reject(); + } else { + const show = () => { + this._stopAnimation(); + this._toggleBodyScroll(this.option('enableBodyScroll')); + this._toggleVisibility(true); + this._$content.css('visibility', 'hidden'); + this._$content.toggleClass(INVISIBLE_STATE_CLASS, false); + this._updateZIndexStackPosition(true); + this._positionController.openingHandled(); + this._renderContent(); + + const showingArgs = { cancel: false }; + this._actions.onShowing(showingArgs); + + const cancelShow = () => { + this._toggleVisibility(false); + this._$content.css('visibility', ''); + this._$content.toggleClass(INVISIBLE_STATE_CLASS, true); + this._isShowingActionCanceled = true; + this._moveFromContainer(); + this._toggleBodyScroll(true); + this.option('visible', false); + this._showingDeferred.resolve(); + }; - this._currentVisible = visible; + const applyShow = () => { + this._$content.css('visibility', ''); + this._renderVisibility(true); + this._animateShowing(); + }; - this._stopAnimation(); + this._processShowingHidingCancel(showingArgs.cancel, applyShow, cancelShow); + }; - if(!visible) { - triggerHidingEvent(this._$content); - } + if (this.option('templatesRenderAsynchronously')) { + this._stopShowTimer(); + // NOTE: T390360, T386038 + this._asyncShowTimeout = setTimeout(show); + } else { + show(); + } + } - if(visible) { - this._checkContainerExists(); - this._moveToContainer(); - this._renderGeometry(); - - triggerShownEvent(this._$content); - triggerResizeEvent(this._$content); - } else { - this._toggleVisibility(visible); - this._$content.toggleClass(INVISIBLE_STATE_CLASS, !visible); - this._updateZIndexStackPosition(visible); - this._moveFromContainer(); - } - this._toggleShading(visible); + return this._showingDeferred.promise(); + }, - this._toggleSubscriptions(visible); - }, + _normalizeAnimation(showHideConfig, direction) { + if (showHideConfig) { + showHideConfig = extend({ + type: 'slide', + skipElementInitialStyles: true, // NOTE: for fadeIn animation + }, showHideConfig); - _updateZIndexStackPosition: function(pushToStack) { - const overlayStack = this._overlayStack(); - const index = overlayStack.indexOf(this); + if (isObject(showHideConfig[direction])) { + extend(showHideConfig[direction], { + position: this._positionController.position, + }); + } + } - if(pushToStack) { - if(index === -1) { - this._zIndex = zIndexPool.create(this._zIndexInitValue()); + return showHideConfig; + }, - overlayStack.push(this); - } + _animateHiding() { + const animation = this._getAnimationConfig() ?? {}; + const hideAnimation = this._normalizeAnimation(animation.hide, 'from'); + const startHideAnimation = hideAnimation?.start ?? noop; + const completeHideAnimation = hideAnimation?.complete ?? noop; - this._$wrapper.css('zIndex', this._zIndex); - this._$content.css('zIndex', this._zIndex); - } else if(index !== -1) { - overlayStack.splice(index, 1); - zIndexPool.remove(this._zIndex); - } - }, + this._animate( + hideAnimation, + (...args) => { + this._$content.css('pointerEvents', ''); + this._renderVisibility(false); - _toggleShading: function(visible) { - this._$wrapper.toggleClass(OVERLAY_SHADER_CLASS, visible && this.option('shading')); + completeHideAnimation.call(this, ...args); + this._hideAnimationProcessing = false; + this._actions?.onHidden(); + + this._hidingDeferred.resolve(); + }, + (...args) => { + this._$content.css('pointerEvents', 'none'); + startHideAnimation.call(this, ...args); + this._hideAnimationProcessing = true; + }, + ); + }, + + _hide() { + if (!this._currentVisible) { + return Deferred().resolve().promise(); + } + this._currentVisible = false; + + this._hidingDeferred = Deferred(); + const hidingArgs = { cancel: false }; + + if (this._isShowingActionCanceled) { + delete this._isShowingActionCanceled; + this._hidingDeferred.reject(); + } else { + this._actions.onHiding(hidingArgs); + + this._toggleSafariScrolling(); + this._toggleBodyScroll(true); + + const cancelHide = () => { + this._isHidingActionCanceled = true; + this._toggleBodyScroll(this.option('enableBodyScroll')); + this.option('visible', true); + this._hidingDeferred.resolve(); + }; + + const applyHide = () => { + this._forceFocusLost(); + this._toggleShading(false); + this._toggleSubscriptions(false); + this._stopShowTimer(); + this._animateHiding(); + }; - this._$wrapper.css('backgroundColor', this.option('shading') ? this.option('shadingColor') : ''); + this._processShowingHidingCancel(hidingArgs.cancel, applyHide, cancelHide); + } + return this._hidingDeferred.promise(); + }, - this._toggleTabTerminator(visible && this.option('shading')); - }, + _forceFocusLost() { + const activeElement = domAdapter.getActiveElement(); + const shouldResetActiveElement = !!this._$content.find(activeElement).length; - _initTabTerminatorHandler: function() { - this._proxiedTabTerminatorHandler = (...args) => { - this._tabKeyHandler(...args); - }; - }, - - _toggleTabTerminator: function(enabled) { - const eventName = addNamespace('keydown', this.NAME); - if(enabled) { - eventsEngine.on(domAdapter.getDocument(), eventName, this._proxiedTabTerminatorHandler); - } else { - eventsEngine.off(domAdapter.getDocument(), eventName, this._proxiedTabTerminatorHandler); - } - }, + if (shouldResetActiveElement) { + resetActiveElement(); + } + }, + + _animate(animation, completeCallback, startCallback) { + if (animation) { + startCallback = startCallback || animation.start || noop; + + fx.animate(this._$content, extend({}, animation, { + start: startCallback, + complete: completeCallback, + })); + } else { + completeCallback(); + } + }, - _findTabbableBounds: function() { - const $elements = this._$wrapper.find('*'); - const elementsCount = $elements.length - 1; - const result = { first: null, last: null }; + _stopAnimation() { + fx.stop(this._$content, true); + }, - for(let i = 0; i <= elementsCount; i++) { - if(!result.first && $elements.eq(i).is(tabbable)) { - result.first = $elements.eq(i); - } + _renderVisibility(visible) { + if (visible && this._isParentHidden()) { + return; + } - if(!result.last && $elements.eq(elementsCount - i).is(tabbable)) { - result.last = $elements.eq(elementsCount - i); - } + this._currentVisible = visible; - if(result.first && result.last) { - break; - } - } + this._stopAnimation(); - return result; - }, + if (!visible) { + triggerHidingEvent(this._$content); + } - _tabKeyHandler: function(e) { - if(normalizeKeyName(e) !== TAB_KEY || !this._isTopOverlay()) { - return; - } + if (visible) { + this._checkContainerExists(); + this._moveToContainer(); + this._renderGeometry(); + + triggerShownEvent(this._$content); + triggerResizeEvent(this._$content); + } else { + this._toggleVisibility(visible); + this._$content.toggleClass(INVISIBLE_STATE_CLASS, !visible); + this._updateZIndexStackPosition(visible); + this._moveFromContainer(); + } + this._toggleShading(visible); - const tabbableElements = this._findTabbableBounds(); + this._toggleSubscriptions(visible); + }, - const $firstTabbable = tabbableElements.first; - const $lastTabbable = tabbableElements.last; + _updateZIndexStackPosition(pushToStack) { + const overlayStack = this._overlayStack(); + const index = overlayStack.indexOf(this); - const isTabOnLast = !e.shiftKey && e.target === $lastTabbable.get(0); - const isShiftTabOnFirst = e.shiftKey && e.target === $firstTabbable.get(0); - const isEmptyTabList = tabbableElements.length === 0; - const isOutsideTarget = !contains(this._$wrapper.get(0), e.target); + if (pushToStack) { + if (index === -1) { + this._zIndex = zIndexPool.create(this._zIndexInitValue()); - if(isTabOnLast || isShiftTabOnFirst || - isEmptyTabList || isOutsideTarget) { + overlayStack.push(this); + } - e.preventDefault(); + this._$wrapper.css('zIndex', this._zIndex); + this._$content.css('zIndex', this._zIndex); + } else if (index !== -1) { + overlayStack.splice(index, 1); + zIndexPool.remove(this._zIndex); + } + }, - const $focusElement = e.shiftKey ? $lastTabbable : $firstTabbable; + _toggleShading(visible) { + this._$wrapper.toggleClass(OVERLAY_SHADER_CLASS, visible && this.option('shading')); - eventsEngine.trigger($focusElement, 'focusin'); - eventsEngine.trigger($focusElement, 'focus'); - } - }, + this._$wrapper.css('backgroundColor', this.option('shading') ? this.option('shadingColor') : ''); - _toggleSubscriptions: function(enabled) { - if(hasWindow()) { - this._toggleHideTopOverlayCallback(enabled); - this._toggleHideOnParentsScrollSubscription(enabled); - } - }, + this._toggleTabTerminator(visible && this.option('shading')); + }, - _toggleHideTopOverlayCallback: function(subscribe) { - if(!this._hideTopOverlayHandler) { - return; - } + _initTabTerminatorHandler() { + this._proxiedTabTerminatorHandler = (...args) => { + this._tabKeyHandler(...args); + }; + }, - if(subscribe) { - hideTopOverlayCallback.add(this._hideTopOverlayHandler); - } else { - hideTopOverlayCallback.remove(this._hideTopOverlayHandler); - } - }, + _toggleTabTerminator(enabled) { + const eventName = addNamespace('keydown', this.NAME); + if (enabled) { + eventsEngine.on(domAdapter.getDocument(), eventName, this._proxiedTabTerminatorHandler); + } else { + eventsEngine.off(domAdapter.getDocument(), eventName, this._proxiedTabTerminatorHandler); + } + }, - _toggleHideOnParentsScrollSubscription: function(needSubscribe) { - const scrollEvent = addNamespace('scroll', this.NAME); - const { prevTargets, handler } = this._parentsScrollSubscriptionInfo ?? {}; + _findTabbableBounds() { + const $elements = this._$wrapper.find('*'); + const elementsCount = $elements.length - 1; + const result = { first: null, last: null }; - eventsEngine.off(prevTargets, scrollEvent, handler); + for (let i = 0; i <= elementsCount; i++) { + if (!result.first && $elements.eq(i).is(tabbable)) { + result.first = $elements.eq(i); + } - const hideOnScroll = this.option('hideOnParentScroll'); + if (!result.last && $elements.eq(elementsCount - i).is(tabbable)) { + result.last = $elements.eq(elementsCount - i); + } - if(needSubscribe && hideOnScroll) { - let $parents = this._getHideOnParentScrollTarget().parents(); - if(devices.real().deviceType === 'desktop') { - $parents = $parents.add(window); - } - eventsEngine.on($parents, scrollEvent, handler); - this._parentsScrollSubscriptionInfo.prevTargets = $parents; - } - }, + if (result.first && result.last) { + break; + } + } - _hideOnParentsScrollHandler: function(e) { - let hideHandled = false; - const hideOnScroll = this.option('hideOnParentScroll'); - if(isFunction(hideOnScroll)) { - hideHandled = hideOnScroll(e); - } + return result; + }, - if(!hideHandled && !this._showAnimationProcessing) { - this.hide(); - } - }, + _tabKeyHandler(e) { + if (normalizeKeyName(e) !== TAB_KEY || !this._isTopOverlay()) { + return; + } - _getHideOnParentScrollTarget: function() { - const $hideOnParentScrollTarget = $(this.option('_hideOnParentScrollTarget')); + const tabbableElements = this._findTabbableBounds(); - if($hideOnParentScrollTarget.length) { - return $hideOnParentScrollTarget; - } + const $firstTabbable = tabbableElements.first; + const $lastTabbable = tabbableElements.last; - return this._$wrapper; - }, + const isTabOnLast = !e.shiftKey && e.target === $lastTabbable.get(0); + const isShiftTabOnFirst = e.shiftKey && e.target === $firstTabbable.get(0); + const isEmptyTabList = tabbableElements.length === 0; + const isOutsideTarget = !contains(this._$wrapper.get(0), e.target); - _render: function() { - this.callBase(); + if (isTabOnLast || isShiftTabOnFirst + || isEmptyTabList || isOutsideTarget) { + e.preventDefault(); - this._appendContentToElement(); - this._renderVisibilityAnimate(this.option('visible')); - }, + const $focusElement = e.shiftKey ? $lastTabbable : $firstTabbable; + // @ts-expect-error + eventsEngine.trigger($focusElement, 'focusin'); + // @ts-expect-error + eventsEngine.trigger($focusElement, 'focus'); + } + }, - _appendContentToElement: function() { - if(!this._$content.parent().is(this.$element())) { - this._$content.appendTo(this.$element()); - } - }, + _toggleSubscriptions(enabled) { + if (hasWindow()) { + this._toggleHideTopOverlayCallback(enabled); + this._toggleHideOnParentsScrollSubscription(enabled); + } + }, - _renderContent: function() { - const shouldDeferRendering = !this._currentVisible && this.option('deferRendering'); - const isParentHidden = this.option('visible') && this._isParentHidden(); + _toggleHideTopOverlayCallback(subscribe) { + if (!this._hideTopOverlayHandler) { + return; + } - if(isParentHidden) { - this._isHidden = true; - return; - } + if (subscribe) { + hideTopOverlayCallback.add(this._hideTopOverlayHandler); + } else { + hideTopOverlayCallback.remove(this._hideTopOverlayHandler); + } + }, - if(this._contentAlreadyRendered || shouldDeferRendering) { - return; - } + _toggleHideOnParentsScrollSubscription(needSubscribe) { + const scrollEvent = addNamespace('scroll', this.NAME); + const { prevTargets, handler } = this._parentsScrollSubscriptionInfo ?? {}; - this._contentAlreadyRendered = true; - this._appendContentToElement(); + eventsEngine.off(prevTargets, scrollEvent, handler); - this.callBase(); - }, + const hideOnScroll = this.option('hideOnParentScroll'); - _isParentHidden: function() { - if(!this.option('_checkParentVisibility')) { - return false; - } + if (needSubscribe && hideOnScroll) { + let $parents = this._getHideOnParentScrollTarget().parents(); + if (devices.real().deviceType === 'desktop') { + $parents = $parents.add(window); + } + eventsEngine.on($parents, scrollEvent, handler); + this._parentsScrollSubscriptionInfo.prevTargets = $parents; + } + }, - if(this._parentHidden !== undefined) { - return this._parentHidden; - } + _hideOnParentsScrollHandler(e) { + let hideHandled = false; + const hideOnScroll = this.option('hideOnParentScroll'); + if (isFunction(hideOnScroll)) { + hideHandled = hideOnScroll(e); + } - const $parent = this.$element().parent(); + if (!hideHandled && !this._showAnimationProcessing) { + this.hide(); + } + }, - if($parent.is(':visible')) { - return false; - } + _getHideOnParentScrollTarget() { + const $hideOnParentScrollTarget = $(this.option('_hideOnParentScrollTarget')); - let isHidden = false; - $parent.add($parent.parents()).each(function() { - const $element = $(this); - if($element.css('display') === 'none') { - isHidden = true; - return false; - } - }); + if ($hideOnParentScrollTarget.length) { + return $hideOnParentScrollTarget; + } - return isHidden || !domAdapter.getBody().contains($parent.get(0)); - }, - - _renderContentImpl: function() { - const whenContentRendered = new Deferred(); - - const contentTemplateOption = this.option('contentTemplate'); - const contentTemplate = this._getTemplate(contentTemplateOption); - const transclude = this._templateManager.anonymousTemplateName === contentTemplateOption; - contentTemplate && contentTemplate.render({ - container: getPublicElement(this.$content()), - noModel: true, - transclude, - onRendered: () => { - whenContentRendered.resolve(); - - // NOTE: T1114344 - if(this.option('templatesRenderAsynchronously')) { - this._dimensionChanged(); - } - } - }); + return this._$wrapper; + }, - this._toggleWrapperScrollEventsSubscription(this.option('preventScrollEvents')); + _render() { + this.callBase(); - whenContentRendered.done(() => { - if(this.option('visible')) { - this._moveToContainer(); - } - }); + this._appendContentToElement(); + this._renderVisibilityAnimate(this.option('visible')); + }, - return whenContentRendered.promise(); - }, - - _getPositionControllerConfig() { - const { container, visualContainer, _fixWrapperPosition, restorePosition, _skipContentPositioning } = this.option(); - // NOTE: position is passed to controller in renderGeometry to prevent window field using in server side mode - - return { - container, - visualContainer, - $root: this.$element(), - $content: this._$content, - $wrapper: this._$wrapper, - onPositioned: this._actions.onPositioned, - onVisualPositionChanged: this._actions.onVisualPositionChanged, - restorePosition, - _fixWrapperPosition, - _skipContentPositioning - }; - }, - - _initPositionController() { - this._positionController = new OverlayPositionController( - this._getPositionControllerConfig() - ); - }, - - _toggleWrapperScrollEventsSubscription: function(enabled) { - const eventName = addNamespace(dragEventMove, this.NAME); - - eventsEngine.off(this._$wrapper, eventName); - - if(enabled) { - eventsEngine.on(this._$wrapper, eventName, { - validate: function() { - return true; - }, - getDirection: function() { - return 'both'; - }, - _toggleGestureCover: function(toggle) { - if(!toggle) { - this._toggleGestureCoverImpl(toggle); - } - }, - _clearSelection: noop, - isNative: true - }, e => { - const originalEvent = e.originalEvent.originalEvent; - const { type } = originalEvent || {}; - const isWheel = type === 'wheel'; - const isMouseMove = type === 'mousemove'; - const isScrollByWheel = isWheel && !isCommandKeyPressed(e); - e._cancelPreventDefault = true; - - if(originalEvent && e.cancelable !== false && (!isMouseMove && !isWheel || isScrollByWheel)) { - e.preventDefault(); - } - }); - } - }, + _appendContentToElement() { + if (!this._$content.parent().is(this.$element())) { + this._$content.appendTo(this.$element()); + } + }, + + _renderContent() { + const shouldDeferRendering = !this._currentVisible && this.option('deferRendering'); + const isParentHidden = this.option('visible') && this._isParentHidden(); + + if (isParentHidden) { + this._isHidden = true; + return; + } + + if (this._contentAlreadyRendered || shouldDeferRendering) { + return; + } + + this._contentAlreadyRendered = true; + this._appendContentToElement(); + + this.callBase(); + }, + + _isParentHidden() { + if (!this.option('_checkParentVisibility')) { + return false; + } - _moveFromContainer: function() { - this._$content.appendTo(this.$element()); - this._$wrapper.detach(); - }, + if (this._parentHidden !== undefined) { + return this._parentHidden; + } + + const $parent = this.$element().parent(); + + if ($parent.is(':visible')) { + return false; + } - _checkContainerExists() { - const $wrapperContainer = this._positionController.$container; + let isHidden = false; + // @ts-expect-error + $parent.add($parent.parents()).each((index, element) => { + const $element = $(element); + // @ts-expect-error + if ($element.css('display') === 'none') { + isHidden = true; + return false; + } + }); - // NOTE: The container is undefined when DOM is not ready yet. See T1143527 - if($wrapperContainer === undefined) { - return; + return isHidden || !domAdapter.getBody().contains($parent.get(0)); + }, + + _renderContentImpl() { + const whenContentRendered = Deferred(); + + const contentTemplateOption = this.option('contentTemplate'); + const contentTemplate = this._getTemplate(contentTemplateOption); + const transclude = this._templateManager.anonymousTemplateName === contentTemplateOption; + contentTemplate && contentTemplate.render({ + container: getPublicElement(this.$content()), + noModel: true, + transclude, + onRendered: () => { + whenContentRendered.resolve(); + + // NOTE: T1114344 + if (this.option('templatesRenderAsynchronously')) { + this._dimensionChanged(); } + }, + }); + + this._toggleWrapperScrollEventsSubscription(this.option('preventScrollEvents')); - const containerExists = $wrapperContainer.length > 0; + whenContentRendered.done(() => { + if (this.option('visible')) { + this._moveToContainer(); + } + }); - if(!containerExists) { - uiErrors.log('W1021', this.NAME); + return whenContentRendered.promise(); + }, + + _getPositionControllerConfig() { + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + container, visualContainer, _fixWrapperPosition, restorePosition, _skipContentPositioning, + } = this.option(); + // NOTE: position is passed to controller in renderGeometry to prevent window field using in server side mode + + return { + container, + visualContainer, + $root: this.$element(), + $content: this._$content, + $wrapper: this._$wrapper, + onPositioned: this._actions.onPositioned, + onVisualPositionChanged: this._actions.onVisualPositionChanged, + restorePosition, + _fixWrapperPosition, + _skipContentPositioning, + }; + }, + + _initPositionController() { + this._positionController = new OverlayPositionController( + this._getPositionControllerConfig(), + ); + }, + + _toggleWrapperScrollEventsSubscription(enabled) { + const eventName = addNamespace(dragEventMove, this.NAME); + + eventsEngine.off(this._$wrapper, eventName); + + if (enabled) { + eventsEngine.on(this._$wrapper, eventName, { + validate() { + return true; + }, + getDirection() { + return 'both'; + }, + _toggleGestureCover(toggle) { + if (!toggle) { + this._toggleGestureCoverImpl(toggle); + } + }, + _clearSelection: noop, + isNative: true, + }, (e) => { + const { originalEvent } = e.originalEvent; + const { type } = originalEvent || {}; + const isWheel = type === 'wheel'; + const isMouseMove = type === 'mousemove'; + const isScrollByWheel = isWheel && !isCommandKeyPressed(e); + e._cancelPreventDefault = true; + + if (originalEvent && e.cancelable !== false && (!isMouseMove && !isWheel || isScrollByWheel)) { + e.preventDefault(); } - }, + }); + } + }, - _moveToContainer: function() { - const $wrapperContainer = this._positionController.$container; + _moveFromContainer() { + this._$content.appendTo(this.$element()); + this._$wrapper.detach(); + }, - this._$wrapper.appendTo($wrapperContainer); - this._$content.appendTo(this._$wrapper); - }, + _checkContainerExists() { + const $wrapperContainer = this._positionController.$container; - _renderGeometry: function(options) { - const { visible } = this.option(); + // NOTE: The container is undefined when DOM is not ready yet. See T1143527 + if ($wrapperContainer === undefined) { + return; + } - if(visible && hasWindow()) { - this._stopAnimation(); - this._renderGeometryImpl(); - } - }, + const containerExists = $wrapperContainer.length > 0; - _renderGeometryImpl: function() { - // NOTE: position can be specified as a function which needs to be called strict on render start - this._positionController.updatePosition(this._getOptionValue('position')); - this._renderWrapper(); - this._renderDimensions(); - this._renderPosition(); - }, - - _renderPosition() { - this._positionController.positionContent(); - }, - - _isAllWindowCovered: function() { - return isWindow(this._positionController.$visualContainer.get(0)) && this.option('shading'); - }, - - _toggleSafariScrolling: function() { - const visible = this.option('visible'); - const $body = $(domAdapter.getBody()); - const isIosSafari = devices.real().platform === 'ios' && browser.safari; - const isAllWindowCovered = this._isAllWindowCovered(); - const isScrollingPrevented = $body.hasClass(PREVENT_SAFARI_SCROLLING_CLASS); - - const shouldPreventScrolling = !isScrollingPrevented + if (!containerExists) { + uiErrors.log('W1021', this.NAME); + } + }, + + _moveToContainer() { + const $wrapperContainer = this._positionController.$container; + + this._$wrapper.appendTo($wrapperContainer); + this._$content.appendTo(this._$wrapper); + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _renderGeometry(options) { + const { visible } = this.option(); + + if (visible && hasWindow()) { + this._stopAnimation(); + this._renderGeometryImpl(); + } + }, + + _renderGeometryImpl() { + // NOTE: position can be specified as a function which needs to be called strict on render start + this._positionController.updatePosition(this._getOptionValue('position')); + this._renderWrapper(); + this._renderDimensions(); + this._renderPosition(); + }, + + _renderPosition() { + this._positionController.positionContent(); + }, + + _isAllWindowCovered() { + return isWindow(this._positionController.$visualContainer.get(0)) && this.option('shading'); + }, + + _toggleSafariScrolling() { + const visible = this.option('visible'); + const $body = $(domAdapter.getBody()); + const isIosSafari = devices.real().platform === 'ios' && browser.safari; + const isAllWindowCovered = this._isAllWindowCovered(); + const isScrollingPrevented = $body.hasClass(PREVENT_SAFARI_SCROLLING_CLASS); + + const shouldPreventScrolling = !isScrollingPrevented && visible && isAllWindowCovered; - const shouldEnableScrolling = isScrollingPrevented + const shouldEnableScrolling = isScrollingPrevented && (!visible || !isAllWindowCovered || this._disposed); - if(isIosSafari) { - if(shouldEnableScrolling) { - $body.removeClass(PREVENT_SAFARI_SCROLLING_CLASS); - window.scrollTo(0, this._cachedBodyScrollTop); - this._cachedBodyScrollTop = undefined; - } else if(shouldPreventScrolling) { - this._cachedBodyScrollTop = window.pageYOffset; - $body.addClass(PREVENT_SAFARI_SCROLLING_CLASS); - } - } - }, + if (isIosSafari) { + if (shouldEnableScrolling) { + $body.removeClass(PREVENT_SAFARI_SCROLLING_CLASS); + window.scrollTo(0, this._cachedBodyScrollTop); + this._cachedBodyScrollTop = undefined; + } else if (shouldPreventScrolling) { + this._cachedBodyScrollTop = window.pageYOffset; + $body.addClass(PREVENT_SAFARI_SCROLLING_CLASS); + } + } + }, - _renderWrapper: function() { - this._positionController.styleWrapperPosition(); - this._renderWrapperDimensions(); - this._positionController.positionWrapper(); - }, + _renderWrapper() { + this._positionController.styleWrapperPosition(); + this._renderWrapperDimensions(); + this._positionController.positionWrapper(); + }, - _renderWrapperDimensions: function() { - const $visualContainer = this._positionController.$visualContainer; - const documentElement = domAdapter.getDocumentElement(); - const isVisualContainerWindow = isWindow($visualContainer.get(0)); + _renderWrapperDimensions() { + const { $visualContainer } = this._positionController; + const documentElement = domAdapter.getDocumentElement(); + const isVisualContainerWindow = isWindow($visualContainer.get(0)); - const wrapperWidth = isVisualContainerWindow ? documentElement.clientWidth : getOuterWidth($visualContainer); - const wrapperHeight = isVisualContainerWindow ? window.innerHeight : getOuterHeight($visualContainer); + const wrapperWidth = isVisualContainerWindow ? documentElement.clientWidth : getOuterWidth($visualContainer); + const wrapperHeight = isVisualContainerWindow ? window.innerHeight : getOuterHeight($visualContainer); - this._$wrapper.css({ - width: wrapperWidth, - height: wrapperHeight - }); - }, - - _renderDimensions: function() { - const content = this._$content.get(0); - - this._$content.css({ - minWidth: this._getOptionValue('minWidth', content), - maxWidth: this._getOptionValue('maxWidth', content), - minHeight: this._getOptionValue('minHeight', content), - maxHeight: this._getOptionValue('maxHeight', content), - width: this._getOptionValue('width', content), - height: this._getOptionValue('height', content) - }); - }, - - _focusTarget: function() { - return this._$content; - }, - - _attachKeyboardEvents: function() { - this._keyboardListenerId = keyboard.on( - this._$content, - null, - opts => this._keyboardHandler(opts) - ); - }, - - _keyboardHandler: function(options) { - const e = options.originalEvent; - const $target = $(e.target); - - if($target.is(this._$content) || !this.option('ignoreChildEvents')) { - this.callBase(...arguments); - } - }, - - _isVisible: function() { - return this.option('visible'); - }, - - _visibilityChanged: function(visible) { - if(visible) { - if(this.option('visible')) { - this._renderVisibilityAnimate(visible); - } - } else { - this._renderVisibilityAnimate(visible); - } - }, + this._$wrapper.css({ + width: wrapperWidth, + height: wrapperHeight, + }); + }, + + _renderDimensions() { + const content = this._$content.get(0); + + this._$content.css({ + minWidth: this._getOptionValue('minWidth', content), + maxWidth: this._getOptionValue('maxWidth', content), + minHeight: this._getOptionValue('minHeight', content), + maxHeight: this._getOptionValue('maxHeight', content), + width: this._getOptionValue('width', content), + height: this._getOptionValue('height', content), + }); + }, + + _focusTarget() { + return this._$content; + }, + + _attachKeyboardEvents() { + this._keyboardListenerId = keyboard.on( + this._$content, + null, + (opts) => this._keyboardHandler(opts), + ); + }, + + _keyboardHandler(options) { + const e = options.originalEvent; + const $target = $(e.target); + + if ($target.is(this._$content) || !this.option('ignoreChildEvents')) { + this.callBase(...arguments); + } + }, + + _isVisible() { + return this.option('visible'); + }, + + _visibilityChanged(visible) { + if (visible) { + if (this.option('visible')) { + this._renderVisibilityAnimate(visible); + } + } else { + this._renderVisibilityAnimate(visible); + } + }, - _dimensionChanged: function() { - this._renderGeometry(); - }, + _dimensionChanged() { + this._renderGeometry(); + }, - _clean: function() { - const options = this.option(); - if(!this._contentAlreadyRendered && !options.isRenovated) { - this.$content().empty(); - } + _clean() { + const options = this.option(); + if (!this._contentAlreadyRendered && !options.isRenovated) { + this.$content().empty(); + } - this._renderVisibility(false); - this._stopShowTimer(); - this._cleanFocusState(); - }, + this._renderVisibility(false); + this._stopShowTimer(); + this._cleanFocusState(); + }, - _stopShowTimer() { - if(this._asyncShowTimeout) { - clearTimeout(this._asyncShowTimeout); - } + _stopShowTimer() { + if (this._asyncShowTimeout) { + clearTimeout(this._asyncShowTimeout); + } - this._asyncShowTimeout = null; - }, + this._asyncShowTimeout = null; + }, - _dispose: function() { - fx.stop(this._$content, false); - clearTimeout(this._deferShowTimer); + _dispose() { + fx.stop(this._$content, false); + clearTimeout(this._deferShowTimer); - this._toggleViewPortSubscription(false); - this._toggleSubscriptions(false); - this._updateZIndexStackPosition(false); - this._toggleTabTerminator(false); + this._toggleViewPortSubscription(false); + this._toggleSubscriptions(false); + this._updateZIndexStackPosition(false); + this._toggleTabTerminator(false); - this._actions = null; - this._parentsScrollSubscriptionInfo = null; + this._actions = null; + this._parentsScrollSubscriptionInfo = null; - this.callBase(); + this.callBase(); - this._toggleSafariScrolling(); - this.option('visible') && zIndexPool.remove(this._zIndex); - this._$wrapper.remove(); - this._$content.remove(); - }, + this._toggleSafariScrolling(); + this.option('visible') && zIndexPool.remove(this._zIndex); + this._$wrapper.remove(); + this._$content.remove(); + }, - _toggleRTLDirection: function(rtl) { - this._$content.toggleClass(RTL_DIRECTION_CLASS, rtl); - }, + _toggleRTLDirection(rtl) { + this._$content.toggleClass(RTL_DIRECTION_CLASS, rtl); + }, - _optionChanged: function(args) { - const { value, name } = args; + _optionChanged(args) { + const { value, name } = args; - if(this._getActionsList().includes(name)) { - this._initActions(); - return; - } + if (this._getActionsList().includes(name)) { + this._initActions(); + return; + } - switch(name) { - case 'animation': - break; - case 'shading': - this._toggleShading(this.option('visible')); - this._toggleSafariScrolling(); - break; - case 'shadingColor': - this._toggleShading(this.option('visible')); - break; - case 'width': - case 'height': - this._renderGeometry(); - break; - case 'minWidth': - case 'maxWidth': - case 'minHeight': - case 'maxHeight': - this._renderGeometry(); - break; - case 'position': - this._positionController.updatePosition(this.option('position')); - this._positionController.restorePositionOnNextRender(true); - this._renderGeometry(); - this._toggleSafariScrolling(); - break; - case 'visible': - this._renderVisibilityAnimate(value) - .done(() => this._animateDeferred?.resolveWith(this)) - .fail(() => this._animateDeferred?.reject()); - break; - case 'container': - this._positionController.updateContainer(value); - this._invalidate(); - this._toggleSafariScrolling(); - break; - case 'visualContainer': - this._positionController.updateVisualContainer(value); - this._renderWrapper(); - this._toggleSafariScrolling(); - break; - case 'innerOverlay': - this._initInnerOverlayClass(); - break; - case 'deferRendering': - case 'contentTemplate': - this._contentAlreadyRendered = false; - this._clean(); - this._invalidate(); - break; - case 'hideTopOverlayHandler': - this._toggleHideTopOverlayCallback(false); - this._initHideTopOverlayHandler(value); - this._toggleHideTopOverlayCallback(this.option('visible')); - break; - case 'hideOnParentScroll': - case '_hideOnParentScrollTarget': - this._toggleHideOnParentsScrollSubscription(this.option('visible')); - break; - case 'closeOnOutsideClick': - case 'hideOnOutsideClick': - case 'propagateOutsideClick': - break; - case 'rtlEnabled': - this._contentAlreadyRendered = false; - this.callBase(args); - break; - case '_fixWrapperPosition': - this._positionController.fixWrapperPosition = value; - break; - case 'wrapperAttr': - this._renderWrapperAttributes(); - break; - case 'restorePosition': - this._positionController.restorePosition = value; - break; - case 'preventScrollEvents': - this._logDeprecatedPreventScrollEventsInfo(); - this._toggleWrapperScrollEventsSubscription(value); - break; - default: - this.callBase(args); - } - }, + switch (name) { + case 'animation': + break; + case 'shading': + this._toggleShading(this.option('visible')); + this._toggleSafariScrolling(); + break; + case 'shadingColor': + this._toggleShading(this.option('visible')); + break; + case 'width': + case 'height': + this._renderGeometry(); + break; + case 'minWidth': + case 'maxWidth': + case 'minHeight': + case 'maxHeight': + this._renderGeometry(); + break; + case 'position': + this._positionController.updatePosition(this.option('position')); + this._positionController.restorePositionOnNextRender(true); + this._renderGeometry(); + this._toggleSafariScrolling(); + break; + case 'visible': + this._renderVisibilityAnimate(value) + .done(() => this._animateDeferred?.resolveWith(this)) + .fail(() => this._animateDeferred?.reject()); + break; + case 'container': + this._positionController.updateContainer(value); + this._invalidate(); + this._toggleSafariScrolling(); + break; + case 'visualContainer': + this._positionController.updateVisualContainer(value); + this._renderWrapper(); + this._toggleSafariScrolling(); + break; + case 'innerOverlay': + this._initInnerOverlayClass(); + break; + case 'deferRendering': + case 'contentTemplate': + this._contentAlreadyRendered = false; + this._clean(); + this._invalidate(); + break; + case 'hideTopOverlayHandler': + this._toggleHideTopOverlayCallback(false); + this._initHideTopOverlayHandler(value); + this._toggleHideTopOverlayCallback(this.option('visible')); + break; + case 'hideOnParentScroll': + case '_hideOnParentScrollTarget': + this._toggleHideOnParentsScrollSubscription(this.option('visible')); + break; + case 'closeOnOutsideClick': + case 'hideOnOutsideClick': + case 'propagateOutsideClick': + break; + case 'rtlEnabled': + this._contentAlreadyRendered = false; + this.callBase(args); + break; + case '_fixWrapperPosition': + this._positionController.fixWrapperPosition = value; + break; + case 'wrapperAttr': + this._renderWrapperAttributes(); + break; + case 'restorePosition': + this._positionController.restorePosition = value; + break; + case 'preventScrollEvents': + this._logDeprecatedPreventScrollEventsInfo(); + this._toggleWrapperScrollEventsSubscription(value); + break; + default: + this.callBase(args); + } + }, - toggle: function(showing) { - showing = showing === undefined ? !this.option('visible') : showing; - const result = new Deferred(); + toggle(showing) { + showing = showing === undefined ? !this.option('visible') : showing; + const result = Deferred(); - if(showing === this.option('visible')) { - return result.resolveWith(this, [showing]).promise(); - } + if (showing === this.option('visible')) { + return result.resolveWith(this, [showing]).promise(); + } - const animateDeferred = new Deferred(); - this._animateDeferred = animateDeferred; - this.option('visible', showing); - - animateDeferred.promise() - .done(() => { - delete this._animateDeferred; - result.resolveWith(this, [this.option('visible')]); - }) - .fail(() => { - delete this._animateDeferred; - result.reject(); - }); - - return result.promise(); - }, - - $content: function() { - return this._$content; - }, - - show: function() { - return this.toggle(true); - }, - - hide: function() { - return this.toggle(false); - }, - - content: function() { - return getPublicElement(this._$content); - }, - - repaint: function() { - if(this._contentAlreadyRendered) { - this._positionController.restorePositionOnNextRender(true); - this._renderGeometry({ forceStopAnimation: true }); - triggerResizeEvent(this._$content); - } else { - this.callBase(); - } + const animateDeferred = Deferred(); + this._animateDeferred = animateDeferred; + this.option('visible', showing); + + animateDeferred.promise() + // @ts-expect-error + .done(() => { + delete this._animateDeferred; + result.resolveWith(this, [this.option('visible')]); + }) + .fail(() => { + delete this._animateDeferred; + result.reject(); + }); + + return result.promise(); + }, + + $content() { + return this._$content; + }, + + show() { + return this.toggle(true); + }, + + hide() { + return this.toggle(false); + }, + + content() { + return getPublicElement(this._$content); + }, + + repaint() { + if (this._contentAlreadyRendered) { + this._positionController.restorePositionOnNextRender(true); + this._renderGeometry({ forceStopAnimation: true }); + triggerResizeEvent(this._$content); + } else { + this.callBase(); } + }, }); -/** -* @name ui.dxOverlay -* @section utils -*/ -Overlay.baseZIndex = zIndex => { - return zIndexPool.base(zIndex); -}; +Overlay.baseZIndex = (zIndex) => zIndexPool.base(zIndex); registerComponent('dxOverlay', Overlay); diff --git a/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts b/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts index 974c002883d6..31e1042323d2 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts @@ -1,234 +1,266 @@ -import $ from '../../core/renderer'; -import { isDefined, isString, isWindow, isEvent } from '../../core/utils/type'; -import { extend } from '../../core/utils/extend'; -import positionUtils from '../../animation/position'; -import { resetPosition, move, locate } from '../../animation/translator'; -import { getWindow } from '../../core/utils/window'; -import swatch from '../widget/swatch_container'; +import positionUtils from '@js/animation/position'; +import { locate, move, resetPosition } from '@js/animation/translator'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { + isDefined, + // @ts-expect-error + isEvent, + isString, + isWindow, +} from '@js/core/utils/type'; +import { getWindow } from '@js/core/utils/window'; +import swatch from '@js/ui/widget/swatch_container'; const window = getWindow(); const OVERLAY_POSITION_ALIASES = { - 'top': { my: 'top center', at: 'top center' }, - 'bottom': { my: 'bottom center', at: 'bottom center' }, - 'right': { my: 'right center', at: 'right center' }, - 'left': { my: 'left center', at: 'left center' }, - 'center': { my: 'center', at: 'center' }, - 'right bottom': { my: 'right bottom', at: 'right bottom' }, - 'right top': { my: 'right top', at: 'right top' }, - 'left bottom': { my: 'left bottom', at: 'left bottom' }, - 'left top': { my: 'left top', at: 'left top' } + top: { my: 'top center', at: 'top center' }, + bottom: { my: 'bottom center', at: 'bottom center' }, + right: { my: 'right center', at: 'right center' }, + left: { my: 'left center', at: 'left center' }, + center: { my: 'center', at: 'center' }, + 'right bottom': { my: 'right bottom', at: 'right bottom' }, + 'right top': { my: 'right top', at: 'right top' }, + 'left bottom': { my: 'left bottom', at: 'left bottom' }, + 'left top': { my: 'left top', at: 'left top' }, }; const OVERLAY_DEFAULT_BOUNDARY_OFFSET = { h: 0, v: 0 }; class OverlayPositionController { - constructor({ - position, container, visualContainer, - $root, $content, $wrapper, - onPositioned, onVisualPositionChanged, - restorePosition, - _fixWrapperPosition, - _skipContentPositioning - }) { - this._props = { - position, - container, - visualContainer, - restorePosition, - onPositioned, - onVisualPositionChanged, - _fixWrapperPosition, - _skipContentPositioning - }; - - this._$root = $root; - this._$content = $content; - this._$wrapper = $wrapper; - - this._$markupContainer = undefined; - this._$visualContainer = undefined; - - this._shouldRenderContentInitialPosition = true; - this._visualPosition = undefined; - this._initialPosition = undefined; - this._previousVisualPosition = undefined; - - this.updateContainer(container); - this.updatePosition(position); - this.updateVisualContainer(visualContainer); - } + _props: any; - get $container() { - this.updateContainer(); // NOTE: swatch classes can be updated runtime + _$root: dxElementWrapper; - return this._$markupContainer; - } + _$content: dxElementWrapper; - get $visualContainer() { - return this._$visualContainer; - } + _$wrapper: dxElementWrapper; - get position() { - return this._position; - } + _$markupContainer: dxElementWrapper | undefined; - set fixWrapperPosition(fixWrapperPosition) { - this._props._fixWrapperPosition = fixWrapperPosition; + _$visualContainer: dxElementWrapper | undefined; - this.styleWrapperPosition(); - } + _shouldRenderContentInitialPosition: boolean; - set restorePosition(restorePosition) { - this._props.restorePosition = restorePosition; - } + _visualPosition: any; - restorePositionOnNextRender(value) { - // NOTE: no visual position means it's a first render - this._shouldRenderContentInitialPosition = value || !this._visualPosition; - } + _initialPosition: any; - openingHandled() { - const shouldRestorePosition = this._props.restorePosition; + _previousVisualPosition: any; - this.restorePositionOnNextRender(shouldRestorePosition); - } + _position: any; - updatePosition(positionProp) { - this._props.position = positionProp; - this._position = this._normalizePosition(positionProp); + constructor({ + position, container, visualContainer, + $root, $content, $wrapper, + onPositioned, onVisualPositionChanged, + restorePosition, + _fixWrapperPosition, + _skipContentPositioning, + }) { + this._props = { + position, + container, + visualContainer, + restorePosition, + onPositioned, + onVisualPositionChanged, + _fixWrapperPosition, + _skipContentPositioning, + }; - this.updateVisualContainer(); - } + this._$root = $root; + this._$content = $content; + this._$wrapper = $wrapper; - updateContainer(containerProp = this._props.container) { - this._props.container = containerProp; + this._$markupContainer = undefined; + this._$visualContainer = undefined; - this._$markupContainer = containerProp - ? $(containerProp) - : swatch.getSwatchContainer(this._$root); + this._shouldRenderContentInitialPosition = true; + this._visualPosition = undefined; + this._initialPosition = undefined; + this._previousVisualPosition = undefined; - this.updateVisualContainer(this._props.visualContainer); - } + this.updateContainer(container); + this.updatePosition(position); + this.updateVisualContainer(visualContainer); + } - updateVisualContainer(visualContainer = this._props.visualContainer) { - this._props.visualContainer = visualContainer; + get $container() { + this.updateContainer(); // NOTE: swatch classes can be updated runtime - this._$visualContainer = this._getVisualContainer(); - } + return this._$markupContainer; + } - detectVisualPositionChange(event) { - this._updateVisualPositionValue(); - this._raisePositionedEvents(event); - } + get $visualContainer() { + return this._$visualContainer; + } - positionContent() { - if(this._shouldRenderContentInitialPosition) { - this._renderContentInitialPosition(); - } else { - move(this._$content, this._visualPosition); - this.detectVisualPositionChange(); - } - } + get position() { + return this._position; + } - positionWrapper() { - if(this._$visualContainer) { - positionUtils.setup(this._$wrapper, { my: 'top left', at: 'top left', of: this._$visualContainer }); - } - } + set fixWrapperPosition(fixWrapperPosition) { + this._props._fixWrapperPosition = fixWrapperPosition; - styleWrapperPosition() { - const useFixed = isWindow(this.$visualContainer.get(0)) || this._props._fixWrapperPosition; + this.styleWrapperPosition(); + } - const positionStyle = useFixed ? 'fixed' : 'absolute'; - this._$wrapper.css('position', positionStyle); - } + set restorePosition(restorePosition) { + this._props.restorePosition = restorePosition; + } - _updateVisualPositionValue() { - this._previousVisualPosition = this._visualPosition; - this._visualPosition = locate(this._$content); - } + restorePositionOnNextRender(value) { + // NOTE: no visual position means it's a first render + this._shouldRenderContentInitialPosition = value || !this._visualPosition; + } - _renderContentInitialPosition() { - this._renderBoundaryOffset(); - resetPosition(this._$content); - const wrapperOverflow = this._$wrapper.css('overflow'); - this._$wrapper.css('overflow', 'hidden'); + openingHandled() { + const shouldRestorePosition = this._props.restorePosition; - if(!this._props._skipContentPositioning) { - const resultPosition = positionUtils.setup(this._$content, this._position); - this._initialPosition = resultPosition; - } + this.restorePositionOnNextRender(shouldRestorePosition); + } - this._$wrapper.css('overflow', wrapperOverflow); - this.detectVisualPositionChange(); + updatePosition(positionProp) { + this._props.position = positionProp; + this._position = this._normalizePosition(positionProp); + + this.updateVisualContainer(); + } + + updateContainer(containerProp = this._props.container) { + this._props.container = containerProp; + + this._$markupContainer = containerProp + ? $(containerProp) + : swatch.getSwatchContainer(this._$root); + + this.updateVisualContainer(this._props.visualContainer); + } + + updateVisualContainer(visualContainer = this._props.visualContainer) { + this._props.visualContainer = visualContainer; + + this._$visualContainer = this._getVisualContainer(); + } + + detectVisualPositionChange(event?: unknown): void { + this._updateVisualPositionValue(); + this._raisePositionedEvents(event); + } + + positionContent() { + if (this._shouldRenderContentInitialPosition) { + this._renderContentInitialPosition(); + } else { + move(this._$content, this._visualPosition); + this.detectVisualPositionChange(); + } + } + + positionWrapper() { + if (this._$visualContainer) { + // @ts-expect-error + positionUtils.setup(this._$wrapper, { my: 'top left', at: 'top left', of: this._$visualContainer }); } + } + + styleWrapperPosition(): void { + // @ts-expect-error + const useFixed = isWindow(this.$visualContainer.get(0)) || this._props._fixWrapperPosition; + + const positionStyle = useFixed ? 'fixed' : 'absolute'; + this._$wrapper.css('position', positionStyle); + } + + _updateVisualPositionValue() { + this._previousVisualPosition = this._visualPosition; + this._visualPosition = locate(this._$content); + } + + _renderContentInitialPosition() { + this._renderBoundaryOffset(); + resetPosition(this._$content); + // @ts-expect-error + const wrapperOverflow = this._$wrapper.css('overflow'); + this._$wrapper.css('overflow', 'hidden'); + + if (!this._props._skipContentPositioning) { + // @ts-expect-error + const resultPosition = positionUtils.setup(this._$content, this._position); + this._initialPosition = resultPosition; + } + // @ts-expect-error + this._$wrapper.css('overflow', wrapperOverflow); + this.detectVisualPositionChange(); + } - _raisePositionedEvents(event) { - const previousPosition = this._previousVisualPosition; - const newPosition = this._visualPosition; + _raisePositionedEvents(event) { + const previousPosition = this._previousVisualPosition; + const newPosition = this._visualPosition; - const isVisualPositionChanged = previousPosition?.top !== newPosition.top + const isVisualPositionChanged = previousPosition?.top !== newPosition.top || previousPosition?.left !== newPosition.left; - if(isVisualPositionChanged) { - this._props.onVisualPositionChanged({ - previousPosition: previousPosition, - position: newPosition, - event - }); - } - - this._props.onPositioned({ - position: this._initialPosition - }); + if (isVisualPositionChanged) { + this._props.onVisualPositionChanged({ + previousPosition, + position: newPosition, + event, + }); } - _renderBoundaryOffset() { - const boundaryOffset = this._position ?? { boundaryOffset: OVERLAY_DEFAULT_BOUNDARY_OFFSET }; + this._props.onPositioned({ + position: this._initialPosition, + }); + } - this._$content.css('margin', `${boundaryOffset.v}px ${boundaryOffset.h}px`); - } + _renderBoundaryOffset() { + const boundaryOffset = this._position ?? { boundaryOffset: OVERLAY_DEFAULT_BOUNDARY_OFFSET }; - _getVisualContainer() { - const containerProp = this._props.container; - const visualContainerProp = this._props.visualContainer; - const positionOf = isEvent(this._props.position?.of) ? this._props.position.of.target : this._props.position?.of; - - if(visualContainerProp) { - return $(visualContainerProp); - } - if(containerProp) { - return $(containerProp); - } - if(positionOf) { - return $(positionOf); - } - - return $(window); - } + this._$content.css('margin', `${boundaryOffset.v}px ${boundaryOffset.h}px`); + } - _normalizePosition(positionProp) { - const defaultPositionConfig = { - boundaryOffset: OVERLAY_DEFAULT_BOUNDARY_OFFSET - }; + _getVisualContainer() { + const containerProp = this._props.container; + const visualContainerProp = this._props.visualContainer; + const positionOf = isEvent(this._props.position?.of) ? this._props.position.of.target : this._props.position?.of; - if(isDefined(positionProp)) { - return extend(true, {}, defaultPositionConfig, this._positionToObject(positionProp)); - } else { - return defaultPositionConfig; - } + if (visualContainerProp) { + return $(visualContainerProp); + } + if (containerProp) { + return $(containerProp); } + if (positionOf) { + return $(positionOf); + } + // @ts-expect-error + return $(window); + } + + _normalizePosition(positionProp) { + const defaultPositionConfig = { + boundaryOffset: OVERLAY_DEFAULT_BOUNDARY_OFFSET, + }; - _positionToObject(position) { - if(isString(position)) { - return extend({}, OVERLAY_POSITION_ALIASES[position]); - } + if (isDefined(positionProp)) { + return extend(true, {}, defaultPositionConfig, this._positionToObject(positionProp)); + } + return defaultPositionConfig; + } - return position; + _positionToObject(position) { + if (isString(position)) { + return extend({}, OVERLAY_POSITION_ALIASES[position]); } + + return position; + } } export { - OVERLAY_POSITION_ALIASES, - OverlayPositionController + OVERLAY_POSITION_ALIASES, + OverlayPositionController, }; diff --git a/packages/devextreme/js/__internal/ui/overlay/m_utils.ts b/packages/devextreme/js/__internal/ui/overlay/m_utils.ts index 8d8da790aff4..6b8ecc6f78c6 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_utils.ts +++ b/packages/devextreme/js/__internal/ui/overlay/m_utils.ts @@ -1,26 +1,30 @@ -import { getInnerHeight, getOuterHeight } from '../../core/utils/size'; -import $ from '../../core/renderer'; -import { getWindow } from '../../core/utils/window'; -import { isNumeric } from '../../core/utils/type'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { getInnerHeight, getOuterHeight } from '@js/core/utils/size'; +import { isNumeric } from '@js/core/utils/type'; +import { getWindow } from '@js/core/utils/window'; const WINDOW_HEIGHT_PERCENT = 0.9; -export const getElementMaxHeightByWindow = ($element, startLocation) => { - const $window = $(getWindow()); - const { top: elementOffset } = $element.offset(); - let actualOffset; +export const getElementMaxHeightByWindow = ($element: dxElementWrapper, startLocation?: number) => { + // @ts-expect-error + const $window = $(getWindow()); + // @ts-expect-error + const { top: elementOffset } = $element.offset(); + let actualOffset; - if(isNumeric(startLocation)) { - if(startLocation < elementOffset) { - return elementOffset - startLocation; - } else { - actualOffset = getInnerHeight($window) - startLocation + $window.scrollTop(); - } - } else { - const offsetTop = elementOffset - $window.scrollTop(); - const offsetBottom = getInnerHeight($window) - offsetTop - getOuterHeight($element); - actualOffset = Math.max(offsetTop, offsetBottom); + if (isNumeric(startLocation)) { + if (startLocation < elementOffset) { + return elementOffset - startLocation; } + // @ts-expect-error + actualOffset = getInnerHeight($window) - startLocation + $window.scrollTop(); + } else { + // @ts-expect-error + const offsetTop = elementOffset - $window.scrollTop(); + const offsetBottom = getInnerHeight($window) - offsetTop - getOuterHeight($element); + actualOffset = Math.max(offsetTop, offsetBottom); + } - return actualOffset * WINDOW_HEIGHT_PERCENT; + return actualOffset * WINDOW_HEIGHT_PERCENT; }; diff --git a/packages/devextreme/js/__internal/ui/overlay/m_z_index.ts b/packages/devextreme/js/__internal/ui/overlay/m_z_index.ts index e5128a2b518b..6a852027b610 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_z_index.ts +++ b/packages/devextreme/js/__internal/ui/overlay/m_z_index.ts @@ -1,33 +1,31 @@ -import { ensureDefined } from '../../core/utils/common'; +import { ensureDefined } from '@js/core/utils/common'; let baseZIndex = 1500; -let zIndexStack = []; +let zIndexStack: number[] = []; -export const base = (ZIndex) => { - baseZIndex = ensureDefined(ZIndex, baseZIndex); - return baseZIndex; +export const base = (ZIndex?: number) => { + baseZIndex = ensureDefined(ZIndex, baseZIndex); + return baseZIndex; }; export const create = (baseIndex = baseZIndex) => { - const length = zIndexStack.length; - const index = (length ? zIndexStack[length - 1] : baseIndex) + 1; + const { length } = zIndexStack; + const index: number = (length ? zIndexStack[length - 1] : baseIndex) + 1; - zIndexStack.push(index); + zIndexStack.push(index); - return index; + return index; }; export const remove = (zIndex) => { - const position = zIndexStack.indexOf(zIndex); - if(position >= 0) { - zIndexStack.splice(position, 1); - } + const position = zIndexStack.indexOf(zIndex); + if (position >= 0) { + zIndexStack.splice(position, 1); + } }; -export const isLastZIndexInStack = (zIndex) => { - return zIndexStack.length && zIndexStack[zIndexStack.length - 1] === zIndex; -}; +export const isLastZIndexInStack = (zIndex) => zIndexStack.length && zIndexStack[zIndexStack.length - 1] === zIndex; export const clearStack = () => { - zIndexStack = []; + zIndexStack = []; }; diff --git a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts index 8f0264537003..5adf1f5af03d 100644 --- a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts @@ -38,7 +38,6 @@ class SliderTooltipPositionController extends PopoverPositionController { _fitIntoSlider() { // @ts-expect-error const { collisionSide, oversize } = positionUtils.calculate(this._$content, this._position).h; - // @ts-expect-error const { left } = this._visualPosition; const isLeftSide = collisionSide === 'left'; diff --git a/packages/devextreme/js/ui/diagram/ui.diagram.js b/packages/devextreme/js/ui/diagram/ui.diagram.js index add34c14eebb..d0bb7bfd93a5 100644 --- a/packages/devextreme/js/ui/diagram/ui.diagram.js +++ b/packages/devextreme/js/ui/diagram/ui.diagram.js @@ -14,7 +14,7 @@ import eventsEngine from '../../events/core/events_engine'; import { addNamespace } from '../../events/utils/index'; import messageLocalization from '../../localization/message'; import numberLocalization from '../../localization/number'; -import * as zIndexPool from '../overlay/z_index'; +import * as zIndexPool from '../../__internal/ui/overlay/m_z_index'; import Overlay from '../overlay/ui.overlay'; import DiagramToolbar from './ui.diagram.toolbar'; diff --git a/packages/devextreme/js/ui/overlay/ui.overlay.js b/packages/devextreme/js/ui/overlay/ui.overlay.js new file mode 100644 index 000000000000..d350800aaf35 --- /dev/null +++ b/packages/devextreme/js/ui/overlay/ui.overlay.js @@ -0,0 +1,13 @@ +import Overlay from '../../__internal/ui/overlay/m_overlay'; + +export default Overlay; + +/** + * @name dxOverlayOptions.activeStateEnabled + * @hidden + */ + +/** + * @name ui.dxOverlay + * @section utils + */ diff --git a/packages/devextreme/js/ui/popover/popover_position_controller.js b/packages/devextreme/js/ui/popover/popover_position_controller.js index 5160eee536da..d3d6c99054c3 100644 --- a/packages/devextreme/js/ui/popover/popover_position_controller.js +++ b/packages/devextreme/js/ui/popover/popover_position_controller.js @@ -4,7 +4,7 @@ import positionUtils from '../../animation/position'; import { pairToObject } from '../../core/utils/common'; import { borderWidthStyles } from '../../renovation/ui/resizable/utils'; import { getWidth, getHeight } from '../../core/utils/size'; -import { OverlayPositionController } from '../overlay/overlay_position_controller'; +import { OverlayPositionController } from '../../__internal/ui/overlay/m_overlay_position_controller'; const WEIGHT_OF_SIDES = { 'left': -1, diff --git a/packages/devextreme/js/ui/popup/popup_position_controller.js b/packages/devextreme/js/ui/popup/popup_position_controller.js index 83e17451d758..e6005c0da679 100644 --- a/packages/devextreme/js/ui/popup/popup_position_controller.js +++ b/packages/devextreme/js/ui/popup/popup_position_controller.js @@ -2,7 +2,7 @@ import $ from '../../core/renderer'; import { move } from '../../animation/translator'; import { getWindow } from '../../core/utils/window'; import { originalViewPort } from '../../core/utils/view_port'; -import { OverlayPositionController } from '../overlay/overlay_position_controller'; +import { OverlayPositionController } from '../../__internal/ui/overlay/m_overlay_position_controller'; const window = getWindow(); diff --git a/packages/devextreme/js/ui/popup/ui.popup.js b/packages/devextreme/js/ui/popup/ui.popup.js index ac3c111aed5f..ce2024c516be 100644 --- a/packages/devextreme/js/ui/popup/ui.popup.js +++ b/packages/devextreme/js/ui/popup/ui.popup.js @@ -30,7 +30,7 @@ import Overlay from '../overlay/ui.overlay'; import { isMaterialBased, isMaterial, isFluent } from '../themes'; import '../toolbar/ui.toolbar.base'; import resizeObserverSingleton from '../../core/resize_observer'; -import * as zIndexPool from '../overlay/z_index'; +import * as zIndexPool from '../../__internal/ui/overlay/m_z_index'; import { PopupPositionController } from './popup_position_controller'; import { createBodyOverflowManager } from './popup_overflow_manager'; import Guid from '../../core/guid'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/drawer.scenarios.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/drawer.scenarios.tests.js index c251ecc748fa..af522b075b55 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/drawer.scenarios.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/drawer.scenarios.tests.js @@ -3,7 +3,7 @@ import { extend } from 'core/utils/extend'; import { isNumeric } from 'core/utils/type'; import { drawerTesters } from '../../helpers/drawerHelpers.js'; import resizeCallbacks from 'core/utils/resize_callbacks'; -import { clearStack } from 'ui/overlay/z_index'; +import { clearStack } from '__internal/ui/overlay/m_z_index'; import 'ui/file_manager'; import 'ui/color_box'; import 'ui/menu'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js index 000a890deb72..c196d6d4e052 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js @@ -16,7 +16,7 @@ import { hideCallback as hideTopOverlayCallback } from 'mobile/hide_callback'; import errors from 'core/errors'; import uiErrors from 'ui/widget/ui.errors'; import Overlay from 'ui/overlay/ui.overlay'; -import * as zIndex from 'ui/overlay/z_index'; +import * as zIndex from '__internal/ui/overlay/m_z_index'; import 'ui/scroll_view/ui.scrollable'; import selectors from 'ui/widget/selectors'; import swatch from 'ui/widget/swatch_container'; From 4e342957902d167c542d8e2c782c137fc4d5da50 Mon Sep 17 00:00:00 2001 From: Joshua Victoria <133762589+jdvictoria@users.noreply.github.com> Date: Tue, 11 Jun 2024 19:07:08 +0800 Subject: [PATCH 08/32] SelectBox: Set appropriate "aria-invalid" attribute value when input is empty or not (T1230706) (#27402) Signed-off-by: Joshua Victoria <133762589+jdvictoria@users.noreply.github.com> --- .../drop_down_editor/ui.drop_down_editor.js | 3 + .../dropDownEditor.tests.js | 161 +++++++++++++++--- .../selectBox.tests.js | 155 ++++++++++++++--- 3 files changed, 266 insertions(+), 53 deletions(-) diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js index 3560f7c2f7b4..befe1a36a0d7 100644 --- a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js +++ b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js @@ -404,12 +404,15 @@ const DropDownEditor = TextBox.inherit({ }, _integrateInput: function() { + const { isValid } = this.option(); + this._renderFocusState(); this._refreshValueChangeEvent(); this._refreshEvents(); this._refreshEmptinessEvent(); this._setDefaultAria(); this._setFieldAria(); + this._toggleValidationClasses(!isValid); this.option('_onMarkupRendered')?.(); }, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js index 10519dc5b54a..efe6e08d19c1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js @@ -2248,46 +2248,153 @@ QUnit.module('aria accessibility', () => { assert.equal($input.attr('aria-expanded'), 'false', 'aria-expanded property on closed'); }); - QUnit.test('component with fieldTemplate should retain aria attributes after interaction (T1230696, T1230971)', function(assert) { - const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ - dataSource: ['one', 'two', 'three'], - fieldTemplate: (data) => { - return $('
').dxTextBox({ value: data }); - }, - valueChangeEvent: 'keyup', - }).dxValidator({ - validationRules: [ { type: 'required' } ] - }); - let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + [ + { attribute: 'aria-required', value: 'true' }, + { attribute: 'aria-haspopup', value: 'true' }, + { attribute: 'aria-autocomplete', value: 'none' }, + ].forEach(({ attribute, value }) => { + QUnit.test(`component with fieldTemplate should have proper ${attribute} attribute after interaction (T1230696, T1230971)`, function(assert) { + const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ + dataSource: ['one', 'two', 'three'], + fieldTemplate: (data) => { + return $('
').dxTextBox({ value: data }); + }, + valueChangeEvent: 'keyup', + }).dxValidator({ + validationRules: [ { type: 'required' } ] + }); + let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); - assert.strictEqual($input.attr('aria-required'), 'true', 'initial render should have aria-required attribute set to true'); + assert.strictEqual($input.attr(attribute), value, `initial render should have ${attribute} attribute set to ${value}`); - assert.strictEqual($input.attr('aria-haspopup'), 'true', 'initial render should have aria-haspopup attribute set to true'); + keyboardMock($input) + .type('a'); - assert.strictEqual($input.attr('aria-autocomplete'), 'none', 'initial render should have aria-autocomplete attribute set to none'); + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.strictEqual($input.attr(attribute), value, `${attribute} attribute should remain ${value} after typing`); - keyboardMock($input) - .type('a'); + keyboardMock($input) + .caret(1) + .press('backspace'); - $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.strictEqual($input.attr(attribute), value, `${attribute} attribute should remain ${value} after deleting`); + }); + }); + + QUnit.module('aria-invalid', {}, () => { + [ + { valueRequired: true, emptyValue: 'true', nonEmptyValue: undefined }, + { valueRequired: false, emptyValue: undefined, nonEmptyValue: undefined } + ].forEach(({ valueRequired, emptyValue, nonEmptyValue }) => { + QUnit.test(`component with fieldTemplate should have proper aria-invalid attribute when validator is used and value is ${!valueRequired ? 'not' : ''} required (T1230706)`, function(assert) { + const clock = sinon.useFakeTimers(); + + const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ + dataSource: ['one', 'two', 'three'], + searchEnabled: true, + fieldTemplate: 'field', + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + valueChangeEvent: 'keyup', + }).dxValidator({ + validationRules: valueRequired ? [{ type: 'required', message: 'required' }] : [], + }); - assert.strictEqual($input.attr('aria-required'), 'true', 'aria-required attribute should remain true after typing'); + clock.tick(500); - assert.strictEqual($input.attr('aria-haspopup'), 'true', 'aria-haspopup attribute should retain to true after typing'); + let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); - assert.strictEqual($input.attr('aria-autocomplete'), 'none', 'aria-autocomplete attribute should retain to none after typing'); + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `initial render should set aria-invalid to ${nonEmptyValue}`); - keyboardMock($input) - .caret(1) - .press('backspace'); + keyboardMock($input) + .type('a'); - $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), 'a', 'input value is not empty'); + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `input should set 'aria-invalid' to ${nonEmptyValue} after typing`); + + keyboardMock($input) + .caret(1) + .press('backspace'); + + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), '', 'input value is empty'); + assert.strictEqual($input.attr('aria-invalid'), emptyValue, `input should set 'aria-invalid' to ${emptyValue} after deleting`); + + clock.restore(); + }); + }); + + QUnit.test('component with fieldTemplate should not have aria-invalid attribute when validator is not used (T1230706)', function(assert) { + const clock = sinon.useFakeTimers(); + + const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ + dataSource: ['one', 'two', 'three'], + searchEnabled: true, + fieldTemplate: 'field', + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + valueChangeEvent: 'keyup', + }); - assert.strictEqual($input.attr('aria-required'), 'true', 'aria-required attribute should remain true after deleting'); + clock.tick(500); - assert.strictEqual($input.attr('aria-haspopup'), 'true', 'aria-haspopup attribute should retain to true after deleting'); + let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); + + keyboardMock($input) + .type('a'); - assert.strictEqual($input.attr('aria-autocomplete'), 'none', 'aria-autocomplete attribute should retain to none after deleting'); + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), 'a', 'input value is not empty'); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'input should set \'aria-invalid\' to undefined after typing'); + + keyboardMock($input) + .caret(1) + .press('backspace'); + + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), '', 'input value is empty'); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'input should set \'aria-invalid\' to undefined after deleting'); + + clock.restore(); + }); }); QUnit.test('component with fieldTemplate should have proper role attribute after interaction (T1230635)', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js index 8ea1639ef950..301160d70bcc 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js @@ -3294,43 +3294,146 @@ QUnit.module('search', moduleSetup, () => { assert.equal($selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)).val(), 'Name 2', 'selectBox displays right value'); }); - QUnit.test('component with fieldTemplate should retain aria attributes after search and selection (T1230696, T1230971)', function(assert) { - const $selectBox = $('#selectBox').dxSelectBox({ - dataSource: ['a', 'ab', 'abc'], - fieldTemplate: () => { - return $('
').dxTextBox({}); - }, - searchEnabled: true, - searchTimeout: 0, - itemTemplate: () => { - return '
'; - } - }).dxValidator({ - validationRules: [ { type: 'required' } ] + [ + { attribute: 'aria-required', value: 'true' }, + { attribute: 'aria-haspopup', value: 'listbox' }, + { attribute: 'aria-autocomplete', value: 'list' }, + ].forEach(({ attribute, value }) => { + QUnit.test(`component with fieldTemplate should have correct ${attribute} attribute after search and selection (T1230696, T1230971)`, function(assert) { + const $selectBox = $('#selectBox').dxSelectBox({ + dataSource: ['a', 'ab', 'abc'], + fieldTemplate: () => { + return $('
').dxTextBox({}); + }, + searchEnabled: true, + searchTimeout: 0, + itemTemplate: () => { + return '
'; + } + }).dxValidator({ + validationRules: [ { type: 'required' } ] + }); + const selectBox = $selectBox.dxSelectBox('instance'); + let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + + assert.strictEqual($input.attr(attribute), value, `initial render should have ${attribute} attribute set to ${value}`); + + keyboardMock($input) + .type('a'); + + const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(1); + listItem.trigger('dxclick'); + + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr(attribute), value, `${attribute} should stay ${value} after search and selection`); + }); + }); + + QUnit.module('aria-invalid', {}, () => { + [ + { valueRequired: true, emptyValue: 'true', nonEmptyValue: undefined }, + { valueRequired: false, emptyValue: undefined, nonEmptyValue: undefined } + ].forEach(({ valueRequired, emptyValue, nonEmptyValue }) => { + QUnit.test(`component with fieldTemplate should have proper aria-invalid attribute when validator is used and value is ${!valueRequired ? 'not' : ''} required (T1230706)`, function(assert) { + const $selectBox = $('#selectBox').dxSelectBox({ + items: [1, 2, 3], + searchEnabled: true, + fieldTemplate: 'field', + showClearButton: true, + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + }).dxValidator({ + validationRules: valueRequired ? [{ type: 'required', message: 'required' }] : [], + }); + + this.clock.tick(TIME_TO_WAIT); + + const selectBox = $selectBox.dxSelectBox('instance'); + let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `initial render should set aria-invalid to ${nonEmptyValue}`); + + const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(0); + listItem.trigger('dxclick'); + + this.clock.tick(TIME_TO_WAIT); + + assert.equal($input.val(), '1', 'input value is not empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `non empty input value set aria-invalid to ${nonEmptyValue}`); + + const $clearButton = $(toSelector(CLEAR_BUTTON_AREA)); + $($clearButton).trigger('dxclick'); + + this.clock.tick(TIME_TO_WAIT); + + assert.equal($input.val(), '', 'input value is empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), emptyValue, `empty input value set aria-invalid to ${emptyValue}`); + }); }); - let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); - assert.strictEqual($input.attr('aria-required'), 'true', 'initial render should have aria-required attribute set to true'); + QUnit.test('component with fieldTemplate should not have aria-invalid attribute when validator is not used (T1230706)', function(assert) { + const $selectBox = $('#selectBox').dxSelectBox({ + items: [1, 2, 3], + searchEnabled: true, + fieldTemplate: 'field', + showClearButton: true, + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + }); - assert.strictEqual($input.attr('aria-haspopup'), 'listbox', 'initial render should have aria-haspopup attribute set to listbox'); + this.clock.tick(TIME_TO_WAIT); - assert.strictEqual($input.attr('aria-autocomplete'), 'list', 'initial render should have aria-autocomplete attribute set to list'); + const selectBox = $selectBox.dxSelectBox('instance'); + let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); - const selectBox = $selectBox.dxSelectBox('instance'); - const keyboard = keyboardMock($input); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); - keyboard.type('a'); + const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(0); + listItem.trigger('dxclick'); - const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(1); - listItem.trigger('dxclick'); + this.clock.tick(TIME_TO_WAIT); - $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.equal($input.val(), '1', 'input value is not empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); - assert.strictEqual($input.attr('aria-required'), 'true', 'aria-required should stay true after search and selection'); + const $clearButton = $(toSelector(CLEAR_BUTTON_AREA)); + $($clearButton).trigger('dxclick'); - assert.strictEqual($input.attr('aria-haspopup'), 'listbox', 'aria-haspopup should stay to listbox after search and selection'); + this.clock.tick(TIME_TO_WAIT); - assert.strictEqual($input.attr('aria-autocomplete'), 'list', 'aria-autocomplete should stay to list after search and selection'); + assert.equal($input.val(), '', 'input value is empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); + }); }); QUnit.test('component with fieldTemplate should have proper role attribute after search and selection (T1230635)', function(assert) { From c6ca75a8c4c33703448187ae81a67a65d006e55f Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 6 Jun 2024 19:01:48 +0400 Subject: [PATCH 09/32] FileUploader: move files to TS --- .../js/{ui/file_uploader.js => __internal/ui/m_file_uploader.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/file_uploader.js => __internal/ui/m_file_uploader.ts} (100%) diff --git a/packages/devextreme/js/ui/file_uploader.js b/packages/devextreme/js/__internal/ui/m_file_uploader.ts similarity index 100% rename from packages/devextreme/js/ui/file_uploader.js rename to packages/devextreme/js/__internal/ui/m_file_uploader.ts From 0d5a1dcd7f5f6254f3562f1ca9fe9efdd062e419 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 6 Jun 2024 19:28:16 +0400 Subject: [PATCH 10/32] FileUploader: ignore errors after move to TS --- .../ui/context_menu/m_context_menu.ts | 4 +- .../js/__internal/ui/m_file_uploader.ts | 3634 +++++++++-------- .../js/__internal/ui/menu/m_menu.ts | 4 +- .../ui/radio_group/m_radio_group.ts | 1 - .../devextreme/js/__internal/ui/widget.ts | 3 + packages/devextreme/js/ui/file_uploader.js | 22 + 6 files changed, 1880 insertions(+), 1788 deletions(-) create mode 100644 packages/devextreme/js/ui/file_uploader.js diff --git a/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts b/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts index e7dd51f25861..e0e12c0c9910 100644 --- a/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts +++ b/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts @@ -23,7 +23,7 @@ import { name as contextMenuEventName } from '@js/events/contextmenu'; import eventsEngine from '@js/events/core/events_engine'; import holdEvent from '@js/events/hold'; import { addNamespace } from '@js/events/utils/index'; -import type { Item, Properties } from '@js/ui/context_menu'; +import type { Item } from '@js/ui/context_menu'; import type { Properties as OverlayProperties } from '@js/ui/overlay'; import type dxOverlay from '@js/ui/overlay'; import Overlay from '@js/ui/overlay/ui.overlay'; @@ -78,8 +78,6 @@ class ContextMenu extends MenuBase { _actions?: any; - _optionsByReference?: Properties; - _showContextMenuEventHandler?: (event: unknown) => any; getShowEvent(showEventOption: { diff --git a/packages/devextreme/js/__internal/ui/m_file_uploader.ts b/packages/devextreme/js/__internal/ui/m_file_uploader.ts index 5984c373192c..5ab6a24468a7 100644 --- a/packages/devextreme/js/__internal/ui/m_file_uploader.ts +++ b/packages/devextreme/js/__internal/ui/m_file_uploader.ts @@ -1,26 +1,29 @@ -import { getOffset, getWidth } from '../core/utils/size'; -import $ from '../core/renderer'; -import Guid from '../core/guid'; -import { getWindow } from '../core/utils/window'; -import eventsEngine from '../events/core/events_engine'; -import registerComponent from '../core/component_registrator'; -import Callbacks from '../core/utils/callbacks'; -import { isDefined, isFunction, isNumeric } from '../core/utils/type'; -import { each } from '../core/utils/iterator'; -import { extend } from '../core/utils/extend'; -import { Deferred, fromPromise } from '../core/utils/deferred'; -import ajax from '../core/utils/ajax'; -import Editor from './editor/editor'; -import Button from './button'; -import ProgressBar from './progress_bar'; -import devices from '../core/devices'; -import { addNamespace, isTouchEvent } from '../events/utils/index'; -import { name as clickEventName } from '../events/click'; -import messageLocalization from '../localization/message'; -import { isFluent, isMaterial } from './themes'; -import domAdapter from '../core/dom_adapter'; - -// STYLE fileUploader +/* eslint-disable max-classes-per-file */ +import registerComponent from '@js/core/component_registrator'; +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import Guid from '@js/core/guid'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import ajax from '@js/core/utils/ajax'; +import Callbacks from '@js/core/utils/callbacks'; +// @ts-expect-error +import { Deferred, fromPromise } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { getOffset, getWidth } from '@js/core/utils/size'; +import { isDefined, isFunction, isNumeric } from '@js/core/utils/type'; +import { getWindow } from '@js/core/utils/window'; +import { name as clickEventName } from '@js/events/click'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace, isTouchEvent } from '@js/events/utils/index'; +import messageLocalization from '@js/localization/message'; +import type { ButtonStyle, ButtonType } from '@js/ui/button'; +import Button from '@js/ui/button'; +import type { Properties as PublicProperties } from '@js/ui/file_uploader'; +import ProgressBar from '@js/ui/progress_bar'; +import { isFluent, isMaterial } from '@js/ui/themes'; +import Editor from '@ts/ui/editor'; const window = getWindow(); @@ -57,1903 +60,1972 @@ const FILEUPLOADER_CHUNK_META_DATA_NAME = 'chunkMetadata'; const DRAG_EVENT_DELTA = 1; let renderFileUploaderInput = () => $('').attr('type', 'file'); - +// @ts-expect-error const isFormDataSupported = () => !!window.FormData; -class FileUploader extends Editor { - - _supportedKeys() { - const click = e => { - e.preventDefault(); - const $selectButton = this._selectButton.$element(); - eventsEngine.trigger($selectButton, clickEventName); - }; - - return extend(super._supportedKeys(), { - space: click, - enter: click - }); - } - - _setOptionsByReference() { - super._setOptionsByReference(); - - extend(this._optionsByReference, { - value: true - }); - } - - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - chunkSize: 0, - value: [], - - selectButtonText: messageLocalization.format('dxFileUploader-selectFile'), - - uploadButtonText: messageLocalization.format('dxFileUploader-upload'), - - labelText: messageLocalization.format('dxFileUploader-dropFile'), - - name: 'files[]', - - multiple: false, - - accept: '', - - uploadUrl: '/', - - allowCanceling: true, - - showFileList: true, - - progress: 0, - - dialogTrigger: undefined, - - dropZone: undefined, - - readyToUploadMessage: messageLocalization.format('dxFileUploader-readyToUpload'), - - uploadedMessage: messageLocalization.format('dxFileUploader-uploaded'), - - uploadFailedMessage: messageLocalization.format('dxFileUploader-uploadFailedMessage'), - - uploadAbortedMessage: messageLocalization.format('dxFileUploader-uploadAbortedMessage'), - - uploadMode: 'instantly', - - uploadMethod: 'POST', - - uploadHeaders: {}, - - uploadCustomData: {}, - - onBeforeSend: null, - - onUploadStarted: null, - - onUploaded: null, - - onFilesUploaded: null, - - onProgress: null, - - onUploadError: null, - - onUploadAborted: null, - - onDropZoneEnter: null, - - onDropZoneLeave: null, - - allowedFileExtensions: [], - - maxFileSize: 0, - - minFileSize: 0, - - inputAttr: {}, - - invalidFileExtensionMessage: messageLocalization.format('dxFileUploader-invalidFileExtension'), - - invalidMaxFileSizeMessage: messageLocalization.format('dxFileUploader-invalidMaxFileSize'), - - invalidMinFileSizeMessage: messageLocalization.format('dxFileUploader-invalidMinFileSize'), - - - /** - * @name dxFileUploaderOptions.extendSelection - * @type boolean - * @default true - * @hidden - */ - extendSelection: true, - - /** - * @name dxFileUploaderOptions.validationMessageMode - * @hidden - */ - validationMessageMode: 'always', - - /** - * @name dxFileUploaderOptions.validationMessagePosition - * @hidden - */ - - - uploadFile: null, - - uploadChunk: null, - - abortUpload: null, - - validationMessageOffset: { h: 0, v: 0 }, - - hoverStateEnabled: true, - - useNativeInputClick: false, - useDragOver: true, - nativeDropSupported: true, - _uploadButtonType: 'normal', - _buttonStylingMode: 'contained', - }); - } - - _defaultOptionsRules() { - return super._defaultOptionsRules().concat([ - { - device: () => devices.real().deviceType === 'desktop' && !devices.isSimulator(), - options: { - focusStateEnabled: true - } - }, - { - device: [ - { - platform: 'android' - } - ], - options: { - validationMessageOffset: { v: 0 } - } - }, - { - device: () => devices.real().deviceType !== 'desktop', - options: { - useDragOver: false - } - }, - { - device: () => !isFormDataSupported(), - options: { - uploadMode: 'useForm' - } - }, - { - device: () => devices.real().deviceType !== 'desktop', - options: { - nativeDropSupported: false - } - }, - { - device: () => isMaterial(), - options: { - _uploadButtonType: 'default' - } - }, - { - device: () => isFluent(), - options: { - _buttonStylingMode: 'text' - } - } - ]); - } - - _initOptions(options) { - const isLabelTextDefined = 'labelText' in options; - super._initOptions(options); - if(!isLabelTextDefined && !this._shouldDragOverBeRendered()) { - this.option('labelText', ''); - } - } - - _init() { - super._init(); - - this._initFileInput(); - this._initLabel(); - - this._setUploadStrategy(); - this._createFiles(); - this._createBeforeSendAction(); - this._createUploadStartedAction(); - this._createUploadedAction(); - this._createFilesUploadedAction(); - this._createProgressAction(); - this._createUploadErrorAction(); - this._createUploadAbortedAction(); - this._createDropZoneEnterAction(); - this._createDropZoneLeaveAction(); - } - - _setUploadStrategy() { - let strategy = null; - - if(this.option('chunkSize') > 0) { - const uploadChunk = this.option('uploadChunk'); - strategy = uploadChunk && isFunction(uploadChunk) ? new CustomChunksFileUploadStrategy(this) : new DefaultChunksFileUploadStrategy(this); - } else { - const uploadFile = this.option('uploadFile'); - strategy = uploadFile && isFunction(uploadFile) ? new CustomWholeFileUploadStrategy(this) : new DefaultWholeFileUploadStrategy(this); - } - - this._uploadStrategy = strategy; - } - - _initFileInput() { - this._isCustomClickEvent = false; - const { multiple, accept, hint } = this.option(); - - if(!this._$fileInput) { - this._$fileInput = renderFileUploaderInput(); - - eventsEngine.on(this._$fileInput, 'change', this._inputChangeHandler.bind(this)); - eventsEngine.on(this._$fileInput, 'click', e => { - e.stopPropagation(); - this._resetInputValue(); - return this.option('useNativeInputClick') || this._isCustomClickEvent; - }); - } - - const inputProps = { - multiple, - accept, - tabIndex: -1, - }; - - if(isDefined(hint)) { - inputProps.title = hint; - } +export interface Properties extends PublicProperties { + _buttonStylingMode?: ButtonStyle; - this._$fileInput.prop(inputProps); - } + _uploadButtonType?: ButtonType; +} - _inputChangeHandler() { - if(this._doPreventInputChange) { - return; - } +class FileUploader extends Editor { + // Temporary solution. Move to component level + public NAME!: string; - const fileName = this._$fileInput.val().replace(/^.*\\/, ''); - const files = this._$fileInput.prop('files'); + _activeDropZone?: any; - if(files && !files.length && this.option('uploadMode') !== 'useForm') { - return; - } + _selectButton!: Button; - const value = files ? this._getFiles(files) : [{ name: fileName }]; - this._changeValue(value); + _doPreventInputChange?: boolean; - if(this.option('uploadMode') === 'instantly') { - this._uploadFiles(); - } - } + _files?: any; - _shouldFileListBeExtended() { - return this.option('uploadMode') !== 'useForm' && this.option('extendSelection') && this.option('multiple'); - } + _$fileInput?: any; - _changeValue(value) { - const files = this._shouldFileListBeExtended() ? this.option('value').slice() : []; - this.option('value', files.concat(value)); - } + _$filesContainer!: dxElementWrapper; - _getFiles(fileList) { - const values = []; + _$inputWrapper!: dxElementWrapper; - each(fileList, (_, value) => values.push(value)); + _$inputContainer?: dxElementWrapper; - return values; - } + _$inputLabel!: dxElementWrapper; - _getFile(fileData) { - const targetFileValue = isNumeric(fileData) ? this.option('value')[fileData] : fileData; - return this._files.filter(file => file.value === targetFileValue)[0]; - } + _$content?: dxElementWrapper; - _initLabel() { - if(!this._$inputLabel) { - this._$inputLabel = $('
'); - } + _uploadStrategy!: CustomChunksFileUploadStrategy + | DefaultChunksFileUploadStrategy + | CustomWholeFileUploadStrategy + | DefaultWholeFileUploadStrategy; - this._updateInputLabelText(); - } + _preventRecreatingFiles?: any; + + _uploadButton?: Button; + + _totalLoadedFilesSize?: any; + + _totalFilesSize?: any; + + _selectFileDialogHandler?: any; + + _isCustomClickEvent?: any; + + _progressAction?: any; + + _uploadStartedAction?: any; + + _dropZoneLeaveAction?: any; + + _dropZoneEnterAction?: any; + + _uploadErrorAction?: any; + + _uploadAbortedAction?: any; + + _filesUploadedAction?: any; + + _uploadedAction?: any; + + _beforeSendAction?: any; + + _supportedKeys() { + const click = (e) => { + e.preventDefault(); + const $selectButton = this._selectButton.$element(); + // @ts-expect-error + eventsEngine.trigger($selectButton, clickEventName); + }; + + return extend(super._supportedKeys(), { + space: click, + enter: click, + }); + } + + _setOptionsByReference(): void { + super._setOptionsByReference(); + + extend(this._optionsByReference, { + value: true, + }); + } + + _getDefaultOptions(): Properties { + return extend(super._getDefaultOptions(), { + chunkSize: 0, + value: [], + selectButtonText: messageLocalization.format('dxFileUploader-selectFile'), + uploadButtonText: messageLocalization.format('dxFileUploader-upload'), + labelText: messageLocalization.format('dxFileUploader-dropFile'), + name: 'files[]', + multiple: false, + accept: '', + uploadUrl: '/', + allowCanceling: true, + showFileList: true, + progress: 0, + dialogTrigger: undefined, + dropZone: undefined, + readyToUploadMessage: messageLocalization.format('dxFileUploader-readyToUpload'), + uploadedMessage: messageLocalization.format('dxFileUploader-uploaded'), + uploadFailedMessage: messageLocalization.format('dxFileUploader-uploadFailedMessage'), + uploadAbortedMessage: messageLocalization.format('dxFileUploader-uploadAbortedMessage'), + uploadMode: 'instantly', + uploadMethod: 'POST', + uploadHeaders: {}, + uploadCustomData: {}, + onBeforeSend: null, + onUploadStarted: null, + onUploaded: null, + onFilesUploaded: null, + onProgress: null, + onUploadError: null, + onUploadAborted: null, + onDropZoneEnter: null, + onDropZoneLeave: null, + allowedFileExtensions: [], + maxFileSize: 0, + minFileSize: 0, + inputAttr: {}, + invalidFileExtensionMessage: messageLocalization.format('dxFileUploader-invalidFileExtension'), + invalidMaxFileSizeMessage: messageLocalization.format('dxFileUploader-invalidMaxFileSize'), + invalidMinFileSizeMessage: messageLocalization.format('dxFileUploader-invalidMinFileSize'), + extendSelection: true, + validationMessageMode: 'always', + uploadFile: null, + uploadChunk: null, + abortUpload: null, + validationMessageOffset: { h: 0, v: 0 }, + hoverStateEnabled: true, + useNativeInputClick: false, + useDragOver: true, + nativeDropSupported: true, + _uploadButtonType: 'normal', + _buttonStylingMode: 'contained', + }); + } + + _defaultOptionsRules() { + return super._defaultOptionsRules().concat([ + { + device: () => devices.real().deviceType === 'desktop' && !devices.isSimulator(), + options: { + focusStateEnabled: true, + }, + }, + { + device: [ + { + platform: 'android', + }, + ], + options: { + validationMessageOffset: { v: 0 }, + }, + }, + { + device: () => devices.real().deviceType !== 'desktop', + options: { + useDragOver: false, + }, + }, + { + device: () => !isFormDataSupported(), + options: { + uploadMode: 'useForm', + }, + }, + { + device: () => devices.real().deviceType !== 'desktop', + options: { + nativeDropSupported: false, + }, + }, + { + // @ts-expect-error + device: () => isMaterial(), + options: { + _uploadButtonType: 'default', + }, + }, + { + // @ts-expect-error + device: () => isFluent(), + options: { + _buttonStylingMode: 'text', + }, + }, + ]); + } + + _initOptions(options): void { + const isLabelTextDefined = 'labelText' in options; + super._initOptions(options); + if (!isLabelTextDefined && !this._shouldDragOverBeRendered()) { + this.option('labelText', ''); + } + } + + _init(): void { + super._init(); + + this._initFileInput(); + this._initLabel(); + + this._setUploadStrategy(); + this._createFiles(); + this._createBeforeSendAction(); + this._createUploadStartedAction(); + this._createUploadedAction(); + this._createFilesUploadedAction(); + this._createProgressAction(); + this._createUploadErrorAction(); + this._createUploadAbortedAction(); + this._createDropZoneEnterAction(); + this._createDropZoneLeaveAction(); + } + + _setUploadStrategy() { + // @ts-expect-error + if (this.option('chunkSize') > 0) { + const uploadChunk = this.option('uploadChunk'); + this._uploadStrategy = uploadChunk && isFunction(uploadChunk) + ? new CustomChunksFileUploadStrategy(this) + : new DefaultChunksFileUploadStrategy(this); + } else { + const uploadFile = this.option('uploadFile'); + this._uploadStrategy = uploadFile && isFunction(uploadFile) + ? new CustomWholeFileUploadStrategy(this) + : new DefaultWholeFileUploadStrategy(this); + } + } + + _initFileInput() { + this._isCustomClickEvent = false; + const { multiple, accept, hint } = this.option(); + + if (!this._$fileInput) { + this._$fileInput = renderFileUploaderInput(); + + eventsEngine.on(this._$fileInput, 'change', this._inputChangeHandler.bind(this)); + eventsEngine.on(this._$fileInput, 'click', (e) => { + e.stopPropagation(); + this._resetInputValue(); + return this.option('useNativeInputClick') || this._isCustomClickEvent; + }); + } + + const inputProps: { + multiple?: boolean; + accept?: string; + tabIndex?: number; + title?: string; + } = { + multiple, + accept, + tabIndex: -1, + }; + + if (isDefined(hint)) { + inputProps.title = hint; + } + + this._$fileInput.prop(inputProps); + } + + _inputChangeHandler() { + if (this._doPreventInputChange) { + return; + } + + const fileName = this._$fileInput.val().replace(/^.*\\/, ''); + const files = this._$fileInput.prop('files'); + + if (files && !files.length && this.option('uploadMode') !== 'useForm') { + return; + } + + const value = files ? this._getFiles(files) : [{ name: fileName }]; + this._changeValue(value); + + if (this.option('uploadMode') === 'instantly') { + this._uploadFiles(); + } + } + + _shouldFileListBeExtended() { + return this.option('uploadMode') !== 'useForm' && this.option('extendSelection') && this.option('multiple'); + } + + _changeValue(value): void { + // @ts-expect-error + const files = this._shouldFileListBeExtended() ? this.option('value').slice() : []; + this.option('value', files.concat(value)); + } + + _getFiles(fileList: File[]) { + const values: File[] = []; + // @ts-expect-error + each(fileList, (_, value) => values.push(value)); + + return values; + } + + _getFile(fileData) { + // @ts-expect-error + const targetFileValue = isNumeric(fileData) ? this.option('value')[fileData] : fileData; + return this._files.filter((file) => file.value === targetFileValue)[0]; + } + + _initLabel(): void { + if (!this._$inputLabel) { + this._$inputLabel = $('
'); + } + + this._updateInputLabelText(); + } + + _updateInputLabelText(): void { + const correctedValue = this._isInteractionDisabled() ? '' : this.option('labelText'); + // @ts-expect-error + this._$inputLabel.text(correctedValue); + } + + _focusTarget() { + // @ts-expect-error + return this.$element().find(`.${FILEUPLOADER_BUTTON_CLASS}`); + } - _updateInputLabelText() { - const correctedValue = this._isInteractionDisabled() ? '' : this.option('labelText'); - this._$inputLabel.text(correctedValue); - } - - _focusTarget() { - return this.$element().find('.' + FILEUPLOADER_BUTTON_CLASS); - } - - _getSubmitElement() { - return this._$fileInput; - } - - _initMarkup() { - super._initMarkup(); - - this.$element().addClass(FILEUPLOADER_CLASS); + _getSubmitElement() { + return this._$fileInput; + } - this._renderWrapper(); - this._renderInputWrapper(); - this._renderSelectButton(); - this._renderInputContainer(); - this._renderUploadButton(); + _initMarkup(): void { + super._initMarkup(); + // @ts-expect-error + this.$element().addClass(FILEUPLOADER_CLASS); + + this._renderWrapper(); + this._renderInputWrapper(); + this._renderSelectButton(); + this._renderInputContainer(); + this._renderUploadButton(); + + this._preventRecreatingFiles = true; + this._activeDropZone = null; + } + + _render(): void { + this._preventRecreatingFiles = false; + this._attachDragEventHandlers(this._$inputWrapper); + this._attachDragEventHandlers(this.option('dropZone')); - this._preventRecreatingFiles = true; - this._activeDropZone = null; - } - - _render() { - this._preventRecreatingFiles = false; - this._attachDragEventHandlers(this._$inputWrapper); - this._attachDragEventHandlers(this.option('dropZone')); + this._renderFiles(); - this._renderFiles(); + super._render(); + } - super._render(); - } + _createFileProgressBar(file): void { + file.progressBar = this._createProgressBar(file.value.size); + file.progressBar.$element().appendTo(file.$file); + this._initStatusMessage(file); + this._ensureCancelButtonInitialized(file); + } - _createFileProgressBar(file) { - file.progressBar = this._createProgressBar(file.value.size); - file.progressBar.$element().appendTo(file.$file); - this._initStatusMessage(file); - this._ensureCancelButtonInitialized(file); - } - - _setStatusMessage(file, message) { - setTimeout(() => { - if(this.option('showFileList')) { - if(file.$statusMessage) { - file.$statusMessage.text(message); - file.$statusMessage.css('display', ''); - file.progressBar.$element().remove(); - } - } - }, FILEUPLOADER_AFTER_LOAD_DELAY); - } - - _getUploadAbortedStatusMessage() { - return this.option('uploadMode') === 'instantly' - ? this.option('uploadAbortedMessage') - : this.option('readyToUploadMessage'); - } + _setStatusMessage(file, message): void { + setTimeout(() => { + if (this.option('showFileList')) { + if (file.$statusMessage) { + file.$statusMessage.text(message); + file.$statusMessage.css('display', ''); + file.progressBar.$element().remove(); + } + } + }, FILEUPLOADER_AFTER_LOAD_DELAY); + } + + _getUploadAbortedStatusMessage() { + return this.option('uploadMode') === 'instantly' + ? this.option('uploadAbortedMessage') + : this.option('readyToUploadMessage'); + } + + _createFiles(): void { + const value = this.option('value'); + + if (this._files && (value?.length === 0 || !this._shouldFileListBeExtended())) { + this._preventFilesUploading(this._files); + this._files = null; + } + + if (!this._files) { + this._files = []; + } + + each(value?.slice(this._files.length), (_, value) => { + const file = this._createFile(value); + this._validateFile(file); + this._files.push(file); + }); + } + + _preventFilesUploading(files) { + files.forEach((file) => this._uploadStrategy.abortUpload(file)); + } + + _validateFile(file) { + file.isValidFileExtension = this._validateFileExtension(file); + file.isValidMinSize = this._validateMinFileSize(file); + file.isValidMaxSize = this._validateMaxFileSize(file); + } + + _validateFileExtension(file) { + const allowedExtensions = this.option('allowedFileExtensions'); + const accept = this.option('accept'); + const allowedTypes = this._getAllowedFileTypes(accept); + const fileExtension = file.value.name.substring(file.value.name.lastIndexOf('.')).toLowerCase(); + if (accept?.length !== 0 && !this._isFileTypeAllowed(file.value, allowedTypes)) { + return false; + } + if (allowedExtensions?.length === 0) { + return true; + } + // @ts-expect-error + for (let i = 0; i < allowedExtensions.length; i++) { + // @ts-expect-error + if (fileExtension === allowedExtensions[i].toLowerCase()) { + return true; + } + } + return false; + } + + _validateMaxFileSize(file): boolean { + const fileSize = file.value.size; + const maxFileSize = this.option('maxFileSize'); + // @ts-expect-error + return maxFileSize > 0 ? fileSize <= maxFileSize : true; + } + + _validateMinFileSize(file): boolean { + const fileSize = file.value.size; + const minFileSize = this.option('minFileSize'); + // @ts-expect-error + return minFileSize > 0 ? fileSize >= minFileSize : true; + } + + _createBeforeSendAction() { + this._beforeSendAction = this._createActionByOption('onBeforeSend', { excludeValidators: ['readOnly'] }); + } + + _createUploadStartedAction() { + this._uploadStartedAction = this._createActionByOption('onUploadStarted', { excludeValidators: ['readOnly'] }); + } + + _createUploadedAction() { + this._uploadedAction = this._createActionByOption('onUploaded', { excludeValidators: ['readOnly'] }); + } + + _createFilesUploadedAction() { + this._filesUploadedAction = this._createActionByOption('onFilesUploaded', { excludeValidators: ['readOnly'] }); + } + + _createProgressAction() { + this._progressAction = this._createActionByOption('onProgress', { excludeValidators: ['readOnly'] }); + } + + _createUploadAbortedAction() { + this._uploadAbortedAction = this._createActionByOption('onUploadAborted', { excludeValidators: ['readOnly'] }); + } + + _createUploadErrorAction() { + this._uploadErrorAction = this._createActionByOption('onUploadError', { excludeValidators: ['readOnly'] }); + } + + _createDropZoneEnterAction() { + this._dropZoneEnterAction = this._createActionByOption('onDropZoneEnter'); + } + + _createDropZoneLeaveAction() { + this._dropZoneLeaveAction = this._createActionByOption('onDropZoneLeave'); + } + + _createFile(value) { + return { + value, + loadedSize: 0, + onProgress: Callbacks(), + onAbort: Callbacks(), + onLoad: Callbacks(), + onError: Callbacks(), + onLoadStart: Callbacks(), + isValidFileExtension: true, + isValidMaxSize: true, + isValidMinSize: true, + isValid() { + return this.isValidFileExtension && this.isValidMaxSize && this.isValidMinSize; + }, + isInitialized: false, + }; + } + + _resetFileState(file): void { + file.isAborted = false; + file.uploadStarted = false; + file.isStartLoad = false; + file.loadedSize = 0; + file.chunksData = undefined; + file.request = undefined; + } + + _renderFiles() { + const value = this.option('value'); + + if (!this._$filesContainer) { + this._$filesContainer = $('
') + .addClass(FILEUPLOADER_FILES_CONTAINER_CLASS) + // @ts-expect-error + .appendTo(this._$content); + } else if (!this._shouldFileListBeExtended() || value?.length === 0) { + this._$filesContainer.empty(); + } + + const showFileList = this.option('showFileList'); + if (showFileList) { + each(this._files, (_, file) => { + if (!file.$file) { + this._renderFile(file); + } + }); + } + // @ts-expect-error + this.$element().toggleClass(FILEUPLOADER_SHOW_FILE_LIST_CLASS, showFileList); + this._toggleFileUploaderEmptyClassName(); + this._updateFileNameMaxWidth(); + + this._validationMessage?.repaint(); + } + + _renderFile(file) { + const { value } = file; + + const $fileContainer = $('
') + .addClass(FILEUPLOADER_FILE_CONTAINER_CLASS) + .appendTo(this._$filesContainer); + + this._renderFileButtons(file, $fileContainer); + + file.$file = $('
') + .addClass(FILEUPLOADER_FILE_CLASS) + .appendTo($fileContainer); + + const $fileInfo = $('
') + .addClass(FILEUPLOADER_FILE_INFO_CLASS) + .appendTo(file.$file); + + file.$statusMessage = $('
') + .addClass(FILEUPLOADER_FILE_STATUS_MESSAGE_CLASS) + .appendTo(file.$file); + + $('
') + .addClass(FILEUPLOADER_FILE_NAME_CLASS) + .text(value.name) + .appendTo($fileInfo); + + if (isDefined(value.size)) { + $('
') + .addClass(FILEUPLOADER_FILE_SIZE_CLASS) + .text(this._getFileSize(value.size)) + .appendTo($fileInfo); + } + + if (file.isValid()) { + file.$statusMessage.text(this.option('readyToUploadMessage')); + } else { + if (!file.isValidFileExtension) { + file.$statusMessage.append(this._createValidationElement('invalidFileExtensionMessage')); + } + if (!file.isValidMaxSize) { + file.$statusMessage.append(this._createValidationElement('invalidMaxFileSizeMessage')); + } + if (!file.isValidMinSize) { + file.$statusMessage.append(this._createValidationElement('invalidMinFileSizeMessage')); + } + $fileContainer.addClass(FILEUPLOADER_INVALID_CLASS); + } + } + + _createValidationElement(key) { + return $('').text(this.option(key)); + } + + _updateFileNameMaxWidth() { + const cancelButtonsCount = this.option('allowCanceling') && this.option('uploadMode') !== 'useForm' ? 1 : 0; + const uploadButtonsCount = this.option('uploadMode') === 'useButtons' ? 1 : 0; + const filesContainerWidth = getWidth( + this._$filesContainer.find(`.${FILEUPLOADER_FILE_CONTAINER_CLASS}`).first(), + ) || getWidth(this._$filesContainer); + const $buttonContainer = this._$filesContainer.find(`.${FILEUPLOADER_BUTTON_CONTAINER_CLASS}`).eq(0); + const buttonsWidth = getWidth($buttonContainer) * (cancelButtonsCount + uploadButtonsCount); + const $fileSize = this._$filesContainer.find(`.${FILEUPLOADER_FILE_SIZE_CLASS}`).eq(0); + + const prevFileSize = $fileSize.text(); + $fileSize.text('1000 Mb'); + const fileSizeWidth = getWidth($fileSize); + $fileSize.text(prevFileSize); + + this._$filesContainer.find(`.${FILEUPLOADER_FILE_NAME_CLASS}`).css('maxWidth', filesContainerWidth - buttonsWidth - fileSizeWidth); + } + + _renderFileButtons(file, $container) { + const $cancelButton = this._getCancelButton(file); + $cancelButton && $container.append($cancelButton); + + const $uploadButton = this._getUploadButton(file); + $uploadButton && $container.append($uploadButton); + } + + _getCancelButton(file) { + if (this.option('uploadMode') === 'useForm') { + return null; + } + + const { + allowCanceling, + readOnly, + hoverStateEnabled, + // eslint-disable-next-line @typescript-eslint/naming-convention + _buttonStylingMode, + } = this.option(); + + file.cancelButton = this._createComponent( + $('
').addClass(`${FILEUPLOADER_BUTTON_CLASS} ${FILEUPLOADER_CANCEL_BUTTON_CLASS}`), + Button, + { + onClick: () => this._removeFile(file), + icon: 'close', + visible: allowCanceling, + disabled: readOnly, + // @ts-expect-error + integrationOptions: {}, + hoverStateEnabled, + stylingMode: _buttonStylingMode, + }, + ); + + return $('
') + .addClass(FILEUPLOADER_BUTTON_CONTAINER_CLASS) + .append(file.cancelButton.$element()); + } + + _getUploadButton(file) { + if (!file.isValid() || this.option('uploadMode') !== 'useButtons') { + return null; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { hoverStateEnabled, _buttonStylingMode } = this.option(); + + file.uploadButton = this._createComponent( + $('
').addClass(`${FILEUPLOADER_BUTTON_CLASS} ${FILEUPLOADER_UPLOAD_BUTTON_CLASS}`), + Button, + { + onClick: () => this._uploadFile(file), + icon: 'upload', + hoverStateEnabled, + stylingMode: _buttonStylingMode, + }, + ); + + file.onLoadStart.add(() => file.uploadButton.option({ + visible: false, + disabled: true, + })); + + file.onAbort.add(() => file.uploadButton.option({ + visible: true, + disabled: false, + })); + + return $('
') + .addClass(FILEUPLOADER_BUTTON_CONTAINER_CLASS) + .append(file.uploadButton.$element()); + } + + _removeFile(file) { + file.$file?.parent().remove(); + + this._files.splice(this._files.indexOf(file), 1); + // @ts-expect-error + const value = this.option('value').slice(); + value.splice(value.indexOf(file.value), 1); + + this._preventRecreatingFiles = true; + this.option('value', value); + this._preventRecreatingFiles = false; + + this._toggleFileUploaderEmptyClassName(); + this._resetInputValue(true); + } + + removeFile(fileData) { + if (this.option('uploadMode') === 'useForm' || !isDefined(fileData)) { + return; + } + const file = this._getFile(fileData); + if (file) { + if (file.uploadStarted) { + this._preventFilesUploading([file]); + } + this._removeFile(file); + } + } + + _toggleFileUploaderEmptyClassName(): void { + // @ts-expect-error + this.$element().toggleClass(FILEUPLOADER_EMPTY_CLASS, !this._files.length || this._hasInvalidFile(this._files)); + } + + _hasInvalidFile(files): boolean { + for (let i = 0; i < files.length; i++) { + if (!files[i].isValid()) { + return true; + } + } + return false; + } + + _getFileSize(size) { + let i = 0; + const labels = [ + messageLocalization.format('dxFileUploader-bytes'), + messageLocalization.format('dxFileUploader-kb'), + messageLocalization.format('dxFileUploader-Mb'), + messageLocalization.format('dxFileUploader-Gb'), + ]; + const count = labels.length - 1; + + while (i < count && size >= 1024) { + size /= 1024; + i++; + } + + return `${Math.round(size)} ${labels[i]}`; + } + + _renderSelectButton() { + const $button = $('
') + .addClass(FILEUPLOADER_BUTTON_CLASS) + .appendTo(this._$inputWrapper); + + this._selectButton = this._createComponent($button, Button, { + text: this.option('selectButtonText'), + focusStateEnabled: false, + // @ts-expect-error + integrationOptions: {}, + disabled: this.option('readOnly'), + hoverStateEnabled: this.option('hoverStateEnabled'), + }); + this._selectFileDialogHandler = this._selectButtonClickHandler.bind(this); + + // NOTE: click triggering on input 'file' works correctly only in native click handler when device is used + if (devices.real().deviceType === 'desktop') { + this._selectButton.option('onClick', this._selectFileDialogHandler); + } else { + this._attachSelectFileDialogHandler(this._selectButton.$element()); + } + this._attachSelectFileDialogHandler(this.option('dialogTrigger')); + } + + // @ts-expect-error + _selectButtonClickHandler() { + if (this.option('useNativeInputClick')) { + return; + } + + if (this._isInteractionDisabled()) { + return false; + } + + this._isCustomClickEvent = true; + // @ts-expect-error + eventsEngine.trigger(this._$fileInput, 'click'); + this._isCustomClickEvent = false; + } + + _attachSelectFileDialogHandler(target) { + if (!isDefined(target)) { + return; + } + this._detachSelectFileDialogHandler(target); + eventsEngine.on($(target), 'click', this._selectFileDialogHandler); + } + + _detachSelectFileDialogHandler(target) { + if (!isDefined(target)) { + return; + } + eventsEngine.off($(target), 'click', this._selectFileDialogHandler); + } + + _renderUploadButton() { + if (this.option('uploadMode') !== 'useButtons') { + return; + } + + const $uploadButton = $('
') + .addClass(FILEUPLOADER_BUTTON_CLASS) + .addClass(FILEUPLOADER_UPLOAD_BUTTON_CLASS) + // @ts-expect-error + .appendTo(this._$content); + + this._uploadButton = this._createComponent($uploadButton, Button, { + text: this.option('uploadButtonText'), + onClick: this._uploadButtonClickHandler.bind(this), + type: this.option('_uploadButtonType'), + // @ts-expect-error + integrationOptions: {}, + hoverStateEnabled: this.option('hoverStateEnabled'), + }); + } + + _uploadButtonClickHandler() { + this._uploadFiles(); + } + + _shouldDragOverBeRendered() { + return !this.option('readOnly') && (this.option('uploadMode') !== 'useForm' || this.option('nativeDropSupported')); + } + + _isInteractionDisabled() { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return this.option('readOnly') || this.option('disabled'); + } + + _renderInputContainer() { + this._$inputContainer = $('
') + .addClass(FILEUPLOADER_INPUT_CONTAINER_CLASS) + .appendTo(this._$inputWrapper); + + this._$fileInput + .addClass(FILEUPLOADER_INPUT_CLASS); + + this._renderInput(); + + const labelId = `dx-fileuploader-input-label-${new Guid()}`; + + this._$inputLabel + .attr('id', labelId) + .addClass(FILEUPLOADER_INPUT_LABEL_CLASS) + .appendTo(this._$inputContainer); + + this.setAria('labelledby', labelId, this._$fileInput); + } + + _renderInput() { + if (this.option('useNativeInputClick')) { + this._selectButton.option('template', this._selectButtonInputTemplate.bind(this)); + } else { + this._$fileInput.appendTo(this._$inputContainer); + this._selectButton.option('template', 'content'); + } + this._applyInputAttributes(this.option('inputAttr')); + } - _createFiles() { - const value = this.option('value'); + _selectButtonInputTemplate(data, content) { + const $content = $(content); + const $text = $('') + .addClass('dx-button-text') + .text(data.text); - if(this._files && (value.length === 0 || !this._shouldFileListBeExtended())) { - this._preventFilesUploading(this._files); - this._files = null; - } + $content + .append($text) + .append(this._$fileInput); - if(!this._files) { - this._files = []; - } + return $content; + } + + _renderInputWrapper() { + this._$inputWrapper = $('
') + .addClass(FILEUPLOADER_INPUT_WRAPPER_CLASS) + // @ts-expect-error + .appendTo(this._$content); + } + + _detachDragEventHandlers(target) { + if (!isDefined(target)) { + return; + } + eventsEngine.off($(target), addNamespace('', this.NAME)); + } + + _attachDragEventHandlers(target) { + const isCustomTarget = target !== this._$inputWrapper; + if (!isDefined(target) || !this._shouldDragOverBeRendered()) { + return; + } + this._detachDragEventHandlers(target); + target = $(target); + + eventsEngine.on(target, addNamespace('dragenter', this.NAME), this._dragEnterHandler.bind(this, isCustomTarget)); + eventsEngine.on(target, addNamespace('dragover', this.NAME), this._dragOverHandler.bind(this, isCustomTarget)); + eventsEngine.on(target, addNamespace('dragleave', this.NAME), this._dragLeaveHandler.bind(this, isCustomTarget)); + eventsEngine.on(target, addNamespace('drop', this.NAME), this._dropHandler.bind(this, isCustomTarget)); + } + + _applyInputAttributes(customAttributes) { + this._$fileInput.attr(customAttributes); + } + + _useInputForDrop() { + return this.option('nativeDropSupported') && this.option('uploadMode') === 'useForm'; + } + + _getDropZoneElement(isCustomTarget, e) { + // @ts-expect-error + let targetList = isCustomTarget ? Array.from($(this.option('dropZone'))) : [this._$inputWrapper]; + // @ts-expect-error + targetList = targetList.map((element) => $(element).get(0)); + return targetList[targetList.indexOf(e.currentTarget)]; + } - each(value.slice(this._files.length), (_, value) => { - const file = this._createFile(value); - this._validateFile(file); - this._files.push(file); - }); + // @ts-expect-error + _dragEnterHandler(isCustomTarget, e) { + if (this.option('disabled')) { + return false; } - _preventFilesUploading(files) { - files.forEach(file => this._uploadStrategy.abortUpload(file)); + if (!this._useInputForDrop()) { + e.preventDefault(); } - _validateFile(file) { - file.isValidFileExtension = this._validateFileExtension(file); - file.isValidMinSize = this._validateMinFileSize(file); - file.isValidMaxSize = this._validateMaxFileSize(file); + const dropZoneElement = this._getDropZoneElement(isCustomTarget, e); + if (isDefined(dropZoneElement) && this._shouldRaiseDragOver(e, dropZoneElement)) { + this._activeDropZone = dropZoneElement; + this._tryToggleDropZoneActive(true, isCustomTarget, e); } + } - _validateFileExtension(file) { - const allowedExtensions = this.option('allowedFileExtensions'); - const accept = this.option('accept'); - const allowedTypes = this._getAllowedFileTypes(accept); - const fileExtension = file.value.name.substring(file.value.name.lastIndexOf('.')).toLowerCase(); - if(accept.length !== 0 && !this._isFileTypeAllowed(file.value, allowedTypes)) { - return false; - } - if(allowedExtensions.length === 0) { - return true; - } - for(let i = 0; i < allowedExtensions.length; i++) { - if(fileExtension === allowedExtensions[i].toLowerCase()) { - return true; - } - } - return false; - } + _shouldRaiseDragOver(e, dropZoneElement) { + return this._activeDropZone === null + && this.isMouseOverElement(e, dropZoneElement, false) + && e.originalEvent.dataTransfer.types.find((item) => item === 'Files'); + } - _validateMaxFileSize(file) { - const fileSize = file.value.size; - const maxFileSize = this.option('maxFileSize'); - return maxFileSize > 0 ? fileSize <= maxFileSize : true; + _dragOverHandler(isCustomTarget, e) { + if (!this._useInputForDrop()) { + e.preventDefault(); } + e.originalEvent.dataTransfer.dropEffect = 'copy'; - _validateMinFileSize(file) { - const fileSize = file.value.size; - const minFileSize = this.option('minFileSize'); - return minFileSize > 0 ? fileSize >= minFileSize : true; + if (!isCustomTarget) { // only default dropzone has pseudoelements + const dropZoneElement = this._getDropZoneElement(false, e); + if (this._shouldRaiseDragOver(e, dropZoneElement)) { + this._dragEnterHandler(false, e); + } + if (this._shouldRaiseDragLeave(e, false)) { + this._dragLeaveHandler(false, e); + } } + } - _createBeforeSendAction() { - this._beforeSendAction = this._createActionByOption('onBeforeSend', { excludeValidators: ['readOnly'] }); + _dragLeaveHandler(isCustomTarget, e) { + if (!this._useInputForDrop()) { + e.preventDefault(); } - _createUploadStartedAction() { - this._uploadStartedAction = this._createActionByOption('onUploadStarted', { excludeValidators: ['readOnly'] }); + if (this._shouldRaiseDragLeave(e, isCustomTarget)) { + this._tryToggleDropZoneActive(false, isCustomTarget, e); + this._activeDropZone = null; } + } - _createUploadedAction() { - this._uploadedAction = this._createActionByOption('onUploaded', { excludeValidators: ['readOnly'] }); - } + _shouldRaiseDragLeave(e, isCustomTarget) { + return this._activeDropZone !== null && !this.isMouseOverElement(e, this._activeDropZone, !isCustomTarget, -DRAG_EVENT_DELTA); + } - _createFilesUploadedAction() { - this._filesUploadedAction = this._createActionByOption('onFilesUploaded', { excludeValidators: ['readOnly'] }); - } + _tryToggleDropZoneActive(active, isCustom, event) { + const classAction = active ? 'addClass' : 'removeClass'; + const mouseAction = active ? '_dropZoneEnterAction' : '_dropZoneLeaveAction'; - _createProgressAction() { - this._progressAction = this._createActionByOption('onProgress', { excludeValidators: ['readOnly'] }); + this[mouseAction]({ + event, + dropZoneElement: this._activeDropZone, + }); + if (!isCustom) { + this.$element()[classAction](FILEUPLOADER_DRAGOVER_CLASS); } + } - _createUploadAbortedAction() { - this._uploadAbortedAction = this._createActionByOption('onUploadAborted', { excludeValidators: ['readOnly'] }); - } + _dropHandler(isCustomTarget, e) { + this._activeDropZone = null; - _createUploadErrorAction() { - this._uploadErrorAction = this._createActionByOption('onUploadError', { excludeValidators: ['readOnly'] }); + if (!isCustomTarget) { + // @ts-expect-error + this.$element().removeClass(FILEUPLOADER_DRAGOVER_CLASS); } - _createDropZoneEnterAction() { - this._dropZoneEnterAction = this._createActionByOption('onDropZoneEnter'); + if (this._useInputForDrop() || isCustomTarget && this._isInteractionDisabled()) { + return; } - _createDropZoneLeaveAction() { - this._dropZoneLeaveAction = this._createActionByOption('onDropZoneLeave'); - } + e.preventDefault(); - _createFile(value) { - return { - value: value, - loadedSize: 0, - onProgress: Callbacks(), - onAbort: Callbacks(), - onLoad: Callbacks(), - onError: Callbacks(), - onLoadStart: Callbacks(), - isValidFileExtension: true, - isValidMaxSize: true, - isValidMinSize: true, - isValid() { - return this.isValidFileExtension && this.isValidMaxSize && this.isValidMinSize; - }, - isInitialized: false - }; - } + const fileList = e.originalEvent.dataTransfer.files; + const files = this._getFiles(fileList); - _resetFileState(file) { - file.isAborted = false; - file.uploadStarted = false; - file.isStartLoad = false; - file.loadedSize = 0; - file.chunksData = undefined; - file.request = undefined; + if (!this.option('multiple') && files.length > 1 || files.length === 0) { + return; } - _renderFiles() { - const value = this.option('value'); - - if(!this._$filesContainer) { - this._$filesContainer = $('
') - .addClass(FILEUPLOADER_FILES_CONTAINER_CLASS) - .appendTo(this._$content); - } else if(!this._shouldFileListBeExtended() || value.length === 0) { - this._$filesContainer.empty(); - } - - const showFileList = this.option('showFileList'); - if(showFileList) { - each(this._files, (_, file) => { - if(!file.$file) { - this._renderFile(file); - } - }); - } - - this.$element().toggleClass(FILEUPLOADER_SHOW_FILE_LIST_CLASS, showFileList); - this._toggleFileUploaderEmptyClassName(); - this._updateFileNameMaxWidth(); + this._changeValue(files); - this._validationMessage?.repaint(); + if (this.option('uploadMode') === 'instantly') { + this._uploadFiles(); } + } - _renderFile(file) { - const value = file.value; + _areAllFilesLoaded() { + return this._files.every((file) => !file.isValid() || file._isError || file._isLoaded || file.isAborted); + } - const $fileContainer = $('
') - .addClass(FILEUPLOADER_FILE_CONTAINER_CLASS) - .appendTo(this._$filesContainer); - - this._renderFileButtons(file, $fileContainer); - - file.$file = $('
') - .addClass(FILEUPLOADER_FILE_CLASS) - .appendTo($fileContainer); - - const $fileInfo = $('
') - .addClass(FILEUPLOADER_FILE_INFO_CLASS) - .appendTo(file.$file); - - file.$statusMessage = $('
') - .addClass(FILEUPLOADER_FILE_STATUS_MESSAGE_CLASS) - .appendTo(file.$file); - - $('
') - .addClass(FILEUPLOADER_FILE_NAME_CLASS) - .text(value.name) - .appendTo($fileInfo); - - if(isDefined(value.size)) { - $('
') - .addClass(FILEUPLOADER_FILE_SIZE_CLASS) - .text(this._getFileSize(value.size)) - .appendTo($fileInfo); - } - - if(file.isValid()) { - file.$statusMessage.text(this.option('readyToUploadMessage')); - } else { - if(!file.isValidFileExtension) { - file.$statusMessage.append(this._createValidationElement('invalidFileExtensionMessage')); - } - if(!file.isValidMaxSize) { - file.$statusMessage.append(this._createValidationElement('invalidMaxFileSizeMessage')); - } - if(!file.isValidMinSize) { - file.$statusMessage.append(this._createValidationElement('invalidMinFileSizeMessage')); - } - $fileContainer.addClass(FILEUPLOADER_INVALID_CLASS); - } + _handleAllFilesUploaded() { + this._recalculateProgress(); + if (this._areAllFilesLoaded()) { + this._filesUploadedAction(); } - _createValidationElement(key) { - return $('').text(this.option(key)); - } - - _updateFileNameMaxWidth() { - const cancelButtonsCount = this.option('allowCanceling') && this.option('uploadMode') !== 'useForm' ? 1 : 0; - const uploadButtonsCount = this.option('uploadMode') === 'useButtons' ? 1 : 0; - const filesContainerWidth = getWidth( - this._$filesContainer.find('.' + FILEUPLOADER_FILE_CONTAINER_CLASS).first() - ) || getWidth(this._$filesContainer); - const $buttonContainer = this._$filesContainer.find('.' + FILEUPLOADER_BUTTON_CONTAINER_CLASS).eq(0); - const buttonsWidth = getWidth($buttonContainer) * (cancelButtonsCount + uploadButtonsCount); - const $fileSize = this._$filesContainer.find('.' + FILEUPLOADER_FILE_SIZE_CLASS).eq(0); + } - const prevFileSize = $fileSize.text(); - $fileSize.text('1000 Mb'); - const fileSizeWidth = getWidth($fileSize); - $fileSize.text(prevFileSize); - - this._$filesContainer.find('.' + FILEUPLOADER_FILE_NAME_CLASS).css('maxWidth', filesContainerWidth - buttonsWidth - fileSizeWidth); + _getAllowedFileTypes(acceptSting) { + if (!acceptSting.length) { + return []; } - _renderFileButtons(file, $container) { - const $cancelButton = this._getCancelButton(file); - $cancelButton && $container.append($cancelButton); + return acceptSting.split(',').map((item) => item.trim()); + } - const $uploadButton = this._getUploadButton(file); - $uploadButton && $container.append($uploadButton); - } + _isFileTypeAllowed(file, allowedTypes) { + for (let i = 0, n = allowedTypes.length; i < n; i++) { + let allowedType = allowedTypes[i]; - _getCancelButton(file) { - if(this.option('uploadMode') === 'useForm') { - return null; - } + if (allowedType[0] === '.') { + allowedType = allowedType.replace('.', '\\.'); - const { - allowCanceling, - readOnly, - hoverStateEnabled, - _buttonStylingMode, - } = this.option(); - - file.cancelButton = this._createComponent( - $('
').addClass(FILEUPLOADER_BUTTON_CLASS + ' ' + FILEUPLOADER_CANCEL_BUTTON_CLASS), - Button, { - onClick: () => this._removeFile(file), - icon: 'close', - visible: allowCanceling, - disabled: readOnly, - integrationOptions: {}, - hoverStateEnabled: hoverStateEnabled, - stylingMode: _buttonStylingMode, - } - ); - - return $('
') - .addClass(FILEUPLOADER_BUTTON_CONTAINER_CLASS) - .append(file.cancelButton.$element()); - } - - _getUploadButton(file) { - if(!file.isValid() || this.option('uploadMode') !== 'useButtons') { - return null; + if (file.name.match(new RegExp(`${allowedType}$`, 'i'))) { + return true; } + } else { + allowedType = allowedType.replace(new RegExp('\\*', 'g'), ''); - const { hoverStateEnabled, _buttonStylingMode } = this.option(); - - file.uploadButton = this._createComponent( - $('
').addClass(FILEUPLOADER_BUTTON_CLASS + ' ' + FILEUPLOADER_UPLOAD_BUTTON_CLASS), - Button, - { - onClick: () => this._uploadFile(file), - icon: 'upload', - hoverStateEnabled: hoverStateEnabled, - stylingMode: _buttonStylingMode, - } - ); - - file.onLoadStart.add(() => file.uploadButton.option({ - visible: false, - disabled: true - })); - - file.onAbort.add(() => file.uploadButton.option({ - visible: true, - disabled: false - })); - - return $('
') - .addClass(FILEUPLOADER_BUTTON_CONTAINER_CLASS) - .append(file.uploadButton.$element()); - } - - _removeFile(file) { - file.$file?.parent().remove(); - - this._files.splice(this._files.indexOf(file), 1); - - const value = this.option('value').slice(); - value.splice(value.indexOf(file.value), 1); - - this._preventRecreatingFiles = true; - this.option('value', value); - this._preventRecreatingFiles = false; - - this._toggleFileUploaderEmptyClassName(); - this._resetInputValue(true); - } - - removeFile(fileData) { - if(this.option('uploadMode') === 'useForm' || !isDefined(fileData)) { - return; - } - const file = this._getFile(fileData); - if(file) { - if(file.uploadStarted) { - this._preventFilesUploading([file]); - } - this._removeFile(file); + if (file.type.match(new RegExp(allowedType, 'i'))) { + return true; } + } } + return false; + } - _toggleFileUploaderEmptyClassName() { - this.$element().toggleClass(FILEUPLOADER_EMPTY_CLASS, !this._files.length || this._hasInvalidFile(this._files)); - } - _hasInvalidFile(files) { - for(let i = 0; i < files.length; i++) { - if(!files[i].isValid()) { - return true; - } - } - return false; - } - - _getFileSize(size) { - let i = 0; - const labels = [ - messageLocalization.format('dxFileUploader-bytes'), - messageLocalization.format('dxFileUploader-kb'), - messageLocalization.format('dxFileUploader-Mb'), - messageLocalization.format('dxFileUploader-Gb') - ]; - const count = labels.length - 1; - - while(i < count && size >= 1024) { - size /= 1024; - i++; - } + _renderWrapper() { + const $wrapper = $('
') + .addClass(FILEUPLOADER_WRAPPER_CLASS) + .appendTo(this.$element()); - return Math.round(size) + ' ' + labels[i]; - } + const $container = $('
') + .addClass(FILEUPLOADER_CONTAINER_CLASS) + .appendTo($wrapper); - _renderSelectButton() { - const $button = $('
') - .addClass(FILEUPLOADER_BUTTON_CLASS) - .appendTo(this._$inputWrapper); + this._$content = $('
') + .addClass(FILEUPLOADER_CONTENT_CLASS) + .appendTo($container); + } - this._selectButton = this._createComponent($button, Button, { - text: this.option('selectButtonText'), - focusStateEnabled: false, - integrationOptions: {}, - disabled: this.option('readOnly'), - hoverStateEnabled: this.option('hoverStateEnabled') - }); - this._selectFileDialogHandler = this._selectButtonClickHandler.bind(this); + _clean() { + this._$fileInput.detach(); + // @ts-expect-error + delete this._$filesContainer; + this._detachSelectFileDialogHandler(this.option('dialogTrigger')); + this._detachDragEventHandlers(this.option('dropZone')); - // NOTE: click triggering on input 'file' works correctly only in native click handler when device is used - if(devices.real().deviceType === 'desktop') { - this._selectButton.option('onClick', this._selectFileDialogHandler); - } else { - this._attachSelectFileDialogHandler(this._selectButton.$element()); - } - this._attachSelectFileDialogHandler(this.option('dialogTrigger')); + if (this._files) { + this._files.forEach((file) => { + file.$file = null; + file.$statusMessage = null; + }); } - _selectButtonClickHandler() { - if(this.option('useNativeInputClick')) { - return; - } + super._clean(); + } - if(this._isInteractionDisabled()) { - return false; - } - - this._isCustomClickEvent = true; - eventsEngine.trigger(this._$fileInput, 'click'); - this._isCustomClickEvent = false; + abortUpload(fileData) { + if (this.option('uploadMode') === 'useForm') { + return; } - - _attachSelectFileDialogHandler(target) { - if(!isDefined(target)) { - return; - } - this._detachSelectFileDialogHandler(target); - eventsEngine.on($(target), 'click', this._selectFileDialogHandler); + if (isDefined(fileData)) { + const file = this._getFile(fileData); + if (file) { + this._preventFilesUploading([file]); + } + } else { + this._preventFilesUploading(this._files); } + } - _detachSelectFileDialogHandler(target) { - if(!isDefined(target)) { - return; - } - eventsEngine.off($(target), 'click', this._selectFileDialogHandler); + upload(fileData) { + if (this.option('uploadMode') === 'useForm') { + return; } - - _renderUploadButton() { - if(this.option('uploadMode') !== 'useButtons') { - return; - } - - const $uploadButton = $('
') - .addClass(FILEUPLOADER_BUTTON_CLASS) - .addClass(FILEUPLOADER_UPLOAD_BUTTON_CLASS) - .appendTo(this._$content); - - this._uploadButton = this._createComponent($uploadButton, Button, { - text: this.option('uploadButtonText'), - onClick: this._uploadButtonClickHandler.bind(this), - type: this.option('_uploadButtonType'), - integrationOptions: {}, - hoverStateEnabled: this.option('hoverStateEnabled') + if (isDefined(fileData)) { + const file = this._getFile(fileData); + if (file && isFormDataSupported()) { + this._uploadFile(file); + } + } else { + this._uploadFiles(); + } + } + + _uploadFiles() { + if (isFormDataSupported()) { + each(this._files, (_, file) => this._uploadFile(file)); + } + } + + _uploadFile(file) { + this._uploadStrategy.upload(file); + } + + _updateProgressBar(file, loadedFileData) { + file.progressBar && file.progressBar.option({ + value: loadedFileData.loaded, + showStatus: true, + }); + + this._progressAction({ + file: file.value, + segmentSize: loadedFileData.currentSegmentSize, + bytesLoaded: loadedFileData.loaded, + bytesTotal: loadedFileData.total, + event: loadedFileData.event, + request: file.request, + }); + } + + _updateTotalProgress(totalFilesSize, totalLoadedFilesSize) { + let progress = 0; + if (isDefined(totalFilesSize)) { + if (this._files.length > 0 && this._areAllFilesLoaded() && totalFilesSize === 0 && totalLoadedFilesSize === 0) { + progress = this._getProgressValue(1); + } else if (totalFilesSize) { + progress = this._getProgressValue(totalLoadedFilesSize / totalFilesSize); + } + } + this.option('progress', progress); + this._setLoadedSize(totalLoadedFilesSize); + } + + _getProgressValue(ratio) { + return Math.floor(ratio * 100); + } + + _initStatusMessage(file) { + file.$statusMessage.css('display', 'none'); + } + + _ensureCancelButtonInitialized(file) { + if (file.isInitialized) { + return; + } + + file.cancelButton.option('onClick', () => { + this._preventFilesUploading([file]); + this._removeFile(file); + }); + + const hideCancelButton = () => { + setTimeout(() => { + file.cancelButton.option({ + visible: false, }); - } - - _uploadButtonClickHandler() { - this._uploadFiles(); - } - - _shouldDragOverBeRendered() { - return !this.option('readOnly') && (this.option('uploadMode') !== 'useForm' || this.option('nativeDropSupported')); - } - - _isInteractionDisabled() { - return this.option('readOnly') || this.option('disabled'); - } - - _renderInputContainer() { - this._$inputContainer = $('
') - .addClass(FILEUPLOADER_INPUT_CONTAINER_CLASS) - .appendTo(this._$inputWrapper); - - this._$fileInput - .addClass(FILEUPLOADER_INPUT_CLASS); - - this._renderInput(); - - const labelId = `dx-fileuploader-input-label-${new Guid()}`; - - this._$inputLabel - .attr('id', labelId) - .addClass(FILEUPLOADER_INPUT_LABEL_CLASS) - .appendTo(this._$inputContainer); - - this.setAria('labelledby', labelId, this._$fileInput); - } - - _renderInput() { - if(this.option('useNativeInputClick')) { - this._selectButton.option('template', this._selectButtonInputTemplate.bind(this)); - } else { - this._$fileInput.appendTo(this._$inputContainer); - this._selectButton.option('template', 'content'); - } - this._applyInputAttributes(this.option('inputAttr')); - } - - _selectButtonInputTemplate(data, content) { - const $content = $(content); - const $text = $('') - .addClass('dx-button-text') - .text(data.text); - - $content - .append($text) - .append(this._$fileInput); - - return $content; - } - - _renderInputWrapper() { - this._$inputWrapper = $('
') - .addClass(FILEUPLOADER_INPUT_WRAPPER_CLASS) - .appendTo(this._$content); - } - - _detachDragEventHandlers(target) { - if(!isDefined(target)) { - return; - } - eventsEngine.off($(target), addNamespace('', this.NAME)); - } - - _attachDragEventHandlers(target) { - const isCustomTarget = target !== this._$inputWrapper; - if(!isDefined(target) || !this._shouldDragOverBeRendered()) { - return; - } - this._detachDragEventHandlers(target); - target = $(target); - - eventsEngine.on(target, addNamespace('dragenter', this.NAME), this._dragEnterHandler.bind(this, isCustomTarget)); - eventsEngine.on(target, addNamespace('dragover', this.NAME), this._dragOverHandler.bind(this, isCustomTarget)); - eventsEngine.on(target, addNamespace('dragleave', this.NAME), this._dragLeaveHandler.bind(this, isCustomTarget)); - eventsEngine.on(target, addNamespace('drop', this.NAME), this._dropHandler.bind(this, isCustomTarget)); - } - - _applyInputAttributes(customAttributes) { - this._$fileInput.attr(customAttributes); - } - - _useInputForDrop() { - return this.option('nativeDropSupported') && this.option('uploadMode') === 'useForm'; - } - - _getDropZoneElement(isCustomTarget, e) { - let targetList = isCustomTarget ? Array.from($(this.option('dropZone'))) : [this._$inputWrapper]; - targetList = targetList.map(element => $(element).get(0)); - return targetList[targetList.indexOf(e.currentTarget)]; - } - - _dragEnterHandler(isCustomTarget, e) { - if(this.option('disabled')) { - return false; - } - - if(!this._useInputForDrop()) { - e.preventDefault(); - } - - const dropZoneElement = this._getDropZoneElement(isCustomTarget, e); - if(isDefined(dropZoneElement) && this._shouldRaiseDragOver(e, dropZoneElement)) { - this._activeDropZone = dropZoneElement; - this._tryToggleDropZoneActive(true, isCustomTarget, e); - } - } - - _shouldRaiseDragOver(e, dropZoneElement) { - return this._activeDropZone === null - && this.isMouseOverElement(e, dropZoneElement, false) - && e.originalEvent.dataTransfer.types.find(item => item === 'Files'); - } - - _dragOverHandler(isCustomTarget, e) { - if(!this._useInputForDrop()) { - e.preventDefault(); - } - e.originalEvent.dataTransfer.dropEffect = 'copy'; - - if(!isCustomTarget) { // only default dropzone has pseudoelements - const dropZoneElement = this._getDropZoneElement(false, e); - if(this._shouldRaiseDragOver(e, dropZoneElement)) { - this._dragEnterHandler(false, e); - } - if(this._shouldRaiseDragLeave(e, false)) { - this._dragLeaveHandler(false, e); - } - } - } - - _dragLeaveHandler(isCustomTarget, e) { - if(!this._useInputForDrop()) { - e.preventDefault(); - } - - if(this._shouldRaiseDragLeave(e, isCustomTarget)) { - this._tryToggleDropZoneActive(false, isCustomTarget, e); - this._activeDropZone = null; - } - } - - _shouldRaiseDragLeave(e, isCustomTarget) { - return this._activeDropZone !== null && !this.isMouseOverElement(e, this._activeDropZone, !isCustomTarget, -DRAG_EVENT_DELTA); - } - - _tryToggleDropZoneActive(active, isCustom, event) { - const classAction = active ? 'addClass' : 'removeClass'; - const mouseAction = active ? '_dropZoneEnterAction' : '_dropZoneLeaveAction'; - - this[mouseAction]({ - event, - dropZoneElement: this._activeDropZone - }); - if(!isCustom) { - this.$element()[classAction](FILEUPLOADER_DRAGOVER_CLASS); - } - } - - _dropHandler(isCustomTarget, e) { - this._activeDropZone = null; - - if(!isCustomTarget) { - this.$element().removeClass(FILEUPLOADER_DRAGOVER_CLASS); - } - - if(this._useInputForDrop() || isCustomTarget && this._isInteractionDisabled()) { - return; - } - - e.preventDefault(); - - const fileList = e.originalEvent.dataTransfer.files; - const files = this._getFiles(fileList); - - if(!this.option('multiple') && files.length > 1 || files.length === 0) { - return; - } - - this._changeValue(files); + }, FILEUPLOADER_AFTER_LOAD_DELAY); + }; + + file.onLoad.add(hideCancelButton); + file.onError.add(hideCancelButton); + } + + _createProgressBar(fileSize) { + return this._createComponent($('
'), ProgressBar, { + value: undefined, + min: 0, + max: fileSize, + statusFormat: (ratio) => `${this._getProgressValue(ratio)}%`, + showStatus: false, + // @ts-expect-error + statusPosition: 'right', + }); + } + + _getTotalFilesSize() { + if (!this._totalFilesSize) { + this._totalFilesSize = 0; + each(this._files, (_, file) => { + this._totalFilesSize += file.value.size; + }); + } + return this._totalFilesSize; + } + + _getTotalLoadedFilesSize() { + if (!this._totalLoadedFilesSize) { + this._totalLoadedFilesSize = 0; + each(this._files, (_, file) => { + this._totalLoadedFilesSize += file.loadedSize; + }); + } + return this._totalLoadedFilesSize; + } + + _setLoadedSize(value) { + this._totalLoadedFilesSize = value; + } + + _recalculateProgress() { + this._totalFilesSize = 0; + this._totalLoadedFilesSize = 0; + this._updateTotalProgress(this._getTotalFilesSize(), this._getTotalLoadedFilesSize()); + } + + isMouseOverElement(mouseEvent, element, correctPseudoElements, dragEventDelta = DRAG_EVENT_DELTA) { + if (!element) return false; + + const beforeHeight = correctPseudoElements ? parseFloat(window.getComputedStyle(element, ':before').height) : 0; + const afterHeight = correctPseudoElements ? parseFloat(window.getComputedStyle(element, ':after').height) : 0; + const x = getOffset(element).left; + const y = getOffset(element).top + beforeHeight; + const w = element.offsetWidth; + const h = element.offsetHeight - beforeHeight - afterHeight; + const eventX = this._getEventX(mouseEvent); + const eventY = this._getEventY(mouseEvent); + + return (eventX + dragEventDelta) >= x && (eventX - dragEventDelta) < (x + w) && (eventY + dragEventDelta) >= y && (eventY - dragEventDelta) < (y + h); + } + + _getEventX(e) { + return isTouchEvent(e) ? this._getTouchEventX(e) : e.clientX + this._getDocumentScrollLeft(); + } + + _getEventY(e) { + return isTouchEvent(e) ? this._getTouchEventY(e) : e.clientY + this._getDocumentScrollTop(); + } + + _getTouchEventX(e) { + let touchPoint = null; + if (e.changedTouches.length > 0) { + touchPoint = e.changedTouches; + } else if (e.targetTouches.length > 0) { + touchPoint = e.targetTouches; + } + // @ts-expect-error + return touchPoint ? touchPoint[0].pageX : 0; + } + + _getTouchEventY(e) { + let touchPoint = null; + if (e.changedTouches.length > 0) { + touchPoint = e.changedTouches; + } else if (e.targetTouches.length > 0) { + touchPoint = e.targetTouches; + } + // @ts-expect-error + return touchPoint ? touchPoint[0].pageY : 0; + } + + _getDocumentScrollTop() { + const document = domAdapter.getDocument(); + return document.documentElement.scrollTop || document.body.scrollTop; + } + + _getDocumentScrollLeft() { + const document = domAdapter.getDocument(); + return document.documentElement.scrollLeft || document.body.scrollLeft; + } + + _updateReadOnlyState() { + const readOnly = this.option('readOnly'); + this._selectButton.option('disabled', readOnly); + this._files.forEach((file) => file.cancelButton?.option('disabled', readOnly)); + this._updateInputLabelText(); + this._attachDragEventHandlers(this._$inputWrapper); + } + + _updateHoverState() { + const value = this.option('hoverStateEnabled'); + this._selectButton?.option('hoverStateEnabled', value); + this._uploadButton?.option('hoverStateEnabled', value); + this._files.forEach((file) => { + file.uploadButton?.option('hoverStateEnabled', value); + file.cancelButton?.option('hoverStateEnabled', value); + }); + } + + _optionChanged(args) { + const { name, value, previousValue } = args; + + switch (name) { + case 'height': + case 'width': + this._updateFileNameMaxWidth(); + super._optionChanged(args); + break; + case 'value': + !value.length && this._$fileInput.val(''); - if(this.option('uploadMode') === 'instantly') { - this._uploadFiles(); + if (!this._preventRecreatingFiles) { + this._createFiles(); + this._renderFiles(); } - } - - _areAllFilesLoaded() { - return this._files.every(file => !file.isValid() || file._isError || file._isLoaded || file.isAborted); - } - _handleAllFilesUploaded() { this._recalculateProgress(); - if(this._areAllFilesLoaded()) { - this._filesUploadedAction(); - } - } - - _getAllowedFileTypes(acceptSting) { - if(!acceptSting.length) { - return []; - } - return acceptSting.split(',').map(item => item.trim()); - } - - _isFileTypeAllowed(file, allowedTypes) { - for(let i = 0, n = allowedTypes.length; i < n; i++) { - let allowedType = allowedTypes[i]; - - if(allowedType[0] === '.') { - allowedType = allowedType.replace('.', '\\.'); - - if(file.name.match(new RegExp(allowedType + '$', 'i'))) { - return true; - } - } else { - allowedType = allowedType.replace(new RegExp('\\*', 'g'), ''); - - if(file.type.match(new RegExp(allowedType, 'i'))) { - return true; - } - } - } - return false; - } - - _renderWrapper() { - const $wrapper = $('
') - .addClass(FILEUPLOADER_WRAPPER_CLASS) - .appendTo(this.$element()); - - const $container = $('
') - .addClass(FILEUPLOADER_CONTAINER_CLASS) - .appendTo($wrapper); - - this._$content = $('
') - .addClass(FILEUPLOADER_CONTENT_CLASS) - .appendTo($container); - } - - _clean() { - this._$fileInput.detach(); - delete this._$filesContainer; - this._detachSelectFileDialogHandler(this.option('dialogTrigger')); - this._detachDragEventHandlers(this.option('dropZone')); - - if(this._files) { - this._files.forEach(file => { - file.$file = null; - file.$statusMessage = null; - }); - } - - super._clean(); - } - - abortUpload(fileData) { - if(this.option('uploadMode') === 'useForm') { - return; - } - if(isDefined(fileData)) { - const file = this._getFile(fileData); - if(file) { - this._preventFilesUploading([file]); - } - } else { - this._preventFilesUploading(this._files); - } - } - - upload(fileData) { - if(this.option('uploadMode') === 'useForm') { - return; - } - if(isDefined(fileData)) { - const file = this._getFile(fileData); - if(file && isFormDataSupported()) { - this._uploadFile(file); - } - } else { - this._uploadFiles(); - } - } - - _uploadFiles() { - if(isFormDataSupported()) { - each(this._files, (_, file) => this._uploadFile(file)); - } - } - _uploadFile(file) { - this._uploadStrategy.upload(file); - } - _updateProgressBar(file, loadedFileData) { - file.progressBar && file.progressBar.option({ - value: loadedFileData.loaded, - showStatus: true - }); - - this._progressAction({ - file: file.value, - segmentSize: loadedFileData.currentSegmentSize, - bytesLoaded: loadedFileData.loaded, - bytesTotal: loadedFileData.total, - event: loadedFileData.event, - request: file.request - }); - } - _updateTotalProgress(totalFilesSize, totalLoadedFilesSize) { - let progress = 0; - if(isDefined(totalFilesSize)) { - if(this._files.length > 0 && this._areAllFilesLoaded() && totalFilesSize === 0 && totalLoadedFilesSize === 0) { - progress = this._getProgressValue(1); - } else if(totalFilesSize) { - progress = this._getProgressValue(totalLoadedFilesSize / totalFilesSize); - } - } - this.option('progress', progress); - this._setLoadedSize(totalLoadedFilesSize); - } - _getProgressValue(ratio) { - return Math.floor(ratio * 100); - } - - _initStatusMessage(file) { - file.$statusMessage.css('display', 'none'); - } - - _ensureCancelButtonInitialized(file) { - if(file.isInitialized) { - return; - } - - file.cancelButton.option('onClick', () => { - this._preventFilesUploading([file]); - this._removeFile(file); - }); - - const hideCancelButton = () => { - setTimeout(() => { - file.cancelButton.option({ - visible: false - }); - }, FILEUPLOADER_AFTER_LOAD_DELAY); - }; - - file.onLoad.add(hideCancelButton); - file.onError.add(hideCancelButton); - } - - _createProgressBar(fileSize) { - return this._createComponent($('
'), ProgressBar, { - value: undefined, - min: 0, - max: fileSize, - statusFormat: ratio => this._getProgressValue(ratio) + '%', - showStatus: false, - statusPosition: 'right' + super._optionChanged(args); + break; + case 'name': + this._initFileInput(); + super._optionChanged(args); + break; + case 'accept': + this._initFileInput(); + break; + case 'multiple': + this._initFileInput(); + if (!args.value) { + this.clear(); + } + break; + case 'readOnly': + this._updateReadOnlyState(); + super._optionChanged(args); + break; + case 'disabled': + this._updateInputLabelText(); + super._optionChanged(args); + break; + case 'selectButtonText': + this._selectButton.option('text', value); + break; + case 'uploadButtonText': + this._uploadButton && this._uploadButton.option('text', value); + break; + case '_uploadButtonType': + this._uploadButton && this._uploadButton.option('type', value); + break; + case '_buttonStylingMode': + this._files.forEach((file) => { + file.uploadButton?.option('stylingMode', value); + file.cancelButton?.option('stylingMode', value); }); - } - - _getTotalFilesSize() { - if(!this._totalFilesSize) { - this._totalFilesSize = 0; - each(this._files, (_, file) => { - this._totalFilesSize += file.value.size; - }); - } - return this._totalFilesSize; - } - - _getTotalLoadedFilesSize() { - if(!this._totalLoadedFilesSize) { - this._totalLoadedFilesSize = 0; - each(this._files, (_, file) => { - this._totalLoadedFilesSize += file.loadedSize; - }); - } - return this._totalLoadedFilesSize; - } - - _setLoadedSize(value) { - this._totalLoadedFilesSize = value; - } - - _recalculateProgress() { - this._totalFilesSize = 0; - this._totalLoadedFilesSize = 0; - this._updateTotalProgress(this._getTotalFilesSize(), this._getTotalLoadedFilesSize()); - } - - isMouseOverElement(mouseEvent, element, correctPseudoElements, dragEventDelta = DRAG_EVENT_DELTA) { - if(!element) return false; - - const beforeHeight = correctPseudoElements ? parseFloat(window.getComputedStyle(element, ':before').height) : 0; - const afterHeight = correctPseudoElements ? parseFloat(window.getComputedStyle(element, ':after').height) : 0; - const x = getOffset(element).left; - const y = getOffset(element).top + beforeHeight; - const w = element.offsetWidth; - const h = element.offsetHeight - beforeHeight - afterHeight; - const eventX = this._getEventX(mouseEvent); - const eventY = this._getEventY(mouseEvent); - - return (eventX + dragEventDelta) >= x && (eventX - dragEventDelta) < (x + w) && (eventY + dragEventDelta) >= y && (eventY - dragEventDelta) < (y + h); - } - _getEventX(e) { - return isTouchEvent(e) ? this._getTouchEventX(e) : e.clientX + this._getDocumentScrollLeft(); - } - _getEventY(e) { - return isTouchEvent(e) ? this._getTouchEventY(e) : e.clientY + this._getDocumentScrollTop(); - } - _getTouchEventX(e) { - let touchPoint = null; - if(e.changedTouches.length > 0) { - touchPoint = e.changedTouches; - } else if(e.targetTouches.length > 0) { - touchPoint = e.targetTouches; - } - return touchPoint ? touchPoint[0].pageX : 0; - } - _getTouchEventY(e) { - let touchPoint = null; - if(e.changedTouches.length > 0) { - touchPoint = e.changedTouches; - } else if(e.targetTouches.length > 0) { - touchPoint = e.targetTouches; - } - return touchPoint ? touchPoint[0].pageY : 0; - } - _getDocumentScrollTop() { - const document = domAdapter.getDocument(); - return document.documentElement.scrollTop || document.body.scrollTop; - } - _getDocumentScrollLeft() { - const document = domAdapter.getDocument(); - return document.documentElement.scrollLeft || document.body.scrollLeft; - } - - _updateReadOnlyState() { - const readOnly = this.option('readOnly'); - this._selectButton.option('disabled', readOnly); - this._files.forEach(file => file.cancelButton?.option('disabled', readOnly)); + break; + case 'dialogTrigger': + this._detachSelectFileDialogHandler(previousValue); + this._attachSelectFileDialogHandler(value); + break; + case 'dropZone': + this._detachDragEventHandlers(previousValue); + this._attachDragEventHandlers(value); + break; + case 'maxFileSize': + case 'minFileSize': + case 'allowedFileExtensions': + case 'invalidFileExtensionMessage': + case 'invalidMaxFileSizeMessage': + case 'invalidMinFileSizeMessage': + case 'readyToUploadMessage': + case 'uploadedMessage': + case 'uploadFailedMessage': + case 'uploadAbortedMessage': + this._invalidate(); + break; + case 'labelText': this._updateInputLabelText(); + break; + case 'showFileList': + if (!this._preventRecreatingFiles) { + this._renderFiles(); + } + break; + case 'uploadFile': + case 'uploadChunk': + case 'chunkSize': + this._setUploadStrategy(); + break; + case 'abortUpload': + case 'uploadUrl': + case 'progress': + case 'uploadMethod': + case 'uploadHeaders': + case 'uploadCustomData': + case 'extendSelection': + break; + case 'hoverStateEnabled': + this._updateHoverState(); + super._optionChanged(args); + break; + case 'allowCanceling': + case 'uploadMode': + this.clear(); + this._invalidate(); + break; + case 'onBeforeSend': + this._createBeforeSendAction(); + break; + case 'onUploadStarted': + this._createUploadStartedAction(); + break; + case 'onUploaded': + this._createUploadedAction(); + break; + case 'onFilesUploaded': + this._createFilesUploadedAction(); + break; + case 'onProgress': + this._createProgressAction(); + break; + case 'onUploadError': + this._createUploadErrorAction(); + break; + case 'onUploadAborted': + this._createUploadAbortedAction(); + break; + case 'onDropZoneEnter': + this._createDropZoneEnterAction(); + break; + case 'onDropZoneLeave': + this._createDropZoneLeaveAction(); + break; + case 'useNativeInputClick': + this._renderInput(); + break; + case 'useDragOver': this._attachDragEventHandlers(this._$inputWrapper); + break; + case 'nativeDropSupported': + this._invalidate(); + break; + case 'inputAttr': + this._applyInputAttributes(this.option(name)); + break; + case 'hint': + this._initFileInput(); + super._optionChanged(args); + break; + default: + super._optionChanged(args); } + } - _updateHoverState() { - const value = this.option('hoverStateEnabled'); - this._selectButton?.option('hoverStateEnabled', value); - this._uploadButton?.option('hoverStateEnabled', value); - this._files.forEach(file => { - file.uploadButton?.option('hoverStateEnabled', value); - file.cancelButton?.option('hoverStateEnabled', value); - }); - } - - _optionChanged(args) { - const { name, value, previousValue } = args; - - switch(name) { - case 'height': - case 'width': - this._updateFileNameMaxWidth(); - super._optionChanged(args); - break; - case 'value': - !value.length && this._$fileInput.val(''); - - if(!this._preventRecreatingFiles) { - this._createFiles(); - this._renderFiles(); - } - - this._recalculateProgress(); - - super._optionChanged(args); - break; - case 'name': - this._initFileInput(); - super._optionChanged(args); - break; - case 'accept': - this._initFileInput(); - break; - case 'multiple': - this._initFileInput(); - if(!args.value) { - this.clear(); - } - break; - case 'readOnly': - this._updateReadOnlyState(); - super._optionChanged(args); - break; - case 'disabled': - this._updateInputLabelText(); - super._optionChanged(args); - break; - case 'selectButtonText': - this._selectButton.option('text', value); - break; - case 'uploadButtonText': - this._uploadButton && this._uploadButton.option('text', value); - break; - case '_uploadButtonType': - this._uploadButton && this._uploadButton.option('type', value); - break; - case '_buttonStylingMode': - this._files.forEach(file => { - file.uploadButton?.option('stylingMode', value); - file.cancelButton?.option('stylingMode', value); - }); - break; - case 'dialogTrigger': - this._detachSelectFileDialogHandler(previousValue); - this._attachSelectFileDialogHandler(value); - break; - case 'dropZone': - this._detachDragEventHandlers(previousValue); - this._attachDragEventHandlers(value); - break; - case 'maxFileSize': - case 'minFileSize': - case 'allowedFileExtensions': - case 'invalidFileExtensionMessage': - case 'invalidMaxFileSizeMessage': - case 'invalidMinFileSizeMessage': - case 'readyToUploadMessage': - case 'uploadedMessage': - case 'uploadFailedMessage': - case 'uploadAbortedMessage': - this._invalidate(); - break; - case 'labelText': - this._updateInputLabelText(); - break; - case 'showFileList': - if(!this._preventRecreatingFiles) { - this._renderFiles(); - } - break; - case 'uploadFile': - case 'uploadChunk': - case 'chunkSize': - this._setUploadStrategy(); - break; - case 'abortUpload': - case 'uploadUrl': - case 'progress': - case 'uploadMethod': - case 'uploadHeaders': - case 'uploadCustomData': - case 'extendSelection': - break; - case 'hoverStateEnabled': - this._updateHoverState(); - super._optionChanged(args); - break; - case 'allowCanceling': - case 'uploadMode': - this.clear(); - this._invalidate(); - break; - case 'onBeforeSend': - this._createBeforeSendAction(); - break; - case 'onUploadStarted': - this._createUploadStartedAction(); - break; - case 'onUploaded': - this._createUploadedAction(); - break; - case 'onFilesUploaded': - this._createFilesUploadedAction(); - break; - case 'onProgress': - this._createProgressAction(); - break; - case 'onUploadError': - this._createUploadErrorAction(); - break; - case 'onUploadAborted': - this._createUploadAbortedAction(); - break; - case 'onDropZoneEnter': - this._createDropZoneEnterAction(); - break; - case 'onDropZoneLeave': - this._createDropZoneLeaveAction(); - break; - case 'useNativeInputClick': - this._renderInput(); - break; - case 'useDragOver': - this._attachDragEventHandlers(this._$inputWrapper); - break; - case 'nativeDropSupported': - this._invalidate(); - break; - case 'inputAttr': - this._applyInputAttributes(this.option(name)); - break; - case 'hint': - this._initFileInput(); - super._optionChanged(args); - break; - default: - super._optionChanged(args); - } - } - - _resetInputValue(force) { - if(this.option('uploadMode') === 'useForm' && !force) { - return; - } - this._doPreventInputChange = true; - this._$fileInput.val(''); - this._doPreventInputChange = false; + _resetInputValue(force?: boolean) { + if (this.option('uploadMode') === 'useForm' && !force) { + return; } + this._doPreventInputChange = true; + this._$fileInput.val(''); + this._doPreventInputChange = false; + } - clear() { - this.option('value', []); - } + clear() { + this.option('value', []); + } } -///#DEBUG +/// #DEBUG +// @ts-expect-error FileUploader.__internals = { - changeFileInputRenderer(renderer) { - renderFileUploaderInput = renderer; - }, - resetFileInputTag() { - renderFileUploaderInput = () => $('').attr('type', 'file'); - } + changeFileInputRenderer(renderer) { + renderFileUploaderInput = renderer; + }, + resetFileInputTag() { + renderFileUploaderInput = () => $('').attr('type', 'file'); + }, }; -///#ENDDEBUG +/// #ENDDEBUG class FileBlobReader { - constructor(file, chunkSize) { - this.file = file; - this.chunkSize = chunkSize; - this.index = 0; - } + file?: any; - read() { - if(!this.file) { - return null; - } - const result = this.createBlobResult(this.file, this.index, this.chunkSize); - if(result.isCompleted) { - this.file = null; - } - this.index++; - return result; - } + chunkSize?: number; - createBlobResult(file, index, chunkSize) { - const currentPosition = index * chunkSize; - return { - blob: this.sliceFile(file, currentPosition, chunkSize), - index: index, - isCompleted: currentPosition + chunkSize >= file.size - }; - } + index?: number; - sliceFile(file, startPos, length) { - if(file.slice) { - return file.slice(startPos, startPos + length); - } - if(file.webkitSlice) { - return file.webkitSlice(startPos, startPos + length); - } - return null; - } -} + constructor(file, chunkSize) { + this.file = file; + this.chunkSize = chunkSize; + this.index = 0; + } -class FileUploadStrategyBase { - constructor(fileUploader) { - this.fileUploader = fileUploader; + read() { + if (!this.file) { + return null; } - - upload(file) { - if(file.isInitialized && file.isAborted) { - this.fileUploader._resetFileState(file); - } - if(file.isValid() && !file.uploadStarted) { - this._prepareFileBeforeUpload(file); - this._uploadCore(file); - } + const result = this.createBlobResult(this.file, this.index, this.chunkSize); + if (result.isCompleted) { + this.file = null; } + // @ts-expect-error + this.index++; + return result; + } - abortUpload(file) { - if(file._isError || file._isLoaded || file.isAborted || !file.uploadStarted) { - return; - } + createBlobResult(file, index, chunkSize) { + const currentPosition = index * chunkSize; + return { + blob: this.sliceFile(file, currentPosition, chunkSize), + index, + isCompleted: currentPosition + chunkSize >= file.size, + }; + } - file.isAborted = true; - file.request && file.request.abort(); - - if(this._isCustomCallback('abortUpload')) { - const abortUpload = this.fileUploader.option('abortUpload'); - const arg = this._createUploadArgument(file); - - let deferred = null; - try { - const result = abortUpload(file.value, arg); - deferred = fromPromise(result); - } catch(error) { - deferred = new Deferred().reject(error).promise(); - } - - deferred - .done(() => file.onAbort.fire()) - .fail(error => this._handleFileError(file, error)); - } + sliceFile(file, startPos, length) { + if (file.slice) { + return file.slice(startPos, startPos + length); } - - _beforeSend(xhr, file) { - const arg = this._createUploadArgument(file); - this.fileUploader._beforeSendAction({ - request: xhr, - file: file.value, - uploadInfo: arg - }); - file.request = xhr; - } - - _createUploadArgument(file) { - } - - _uploadCore(file) { - } - - _isCustomCallback(name) { - const callback = this.fileUploader.option(name); - return callback && isFunction(callback); - } - - _handleProgress(file, e) { - if(file._isError) { - return; - } - - file._isProgressStarted = true; - this._handleProgressCore(file, e); - } - - _handleProgressCore(file, e) { - } - - _handleFileError(file, error) { - file._isError = true; - file.onError.fire(error); - } - - _prepareFileBeforeUpload(file) { - if(file.$file) { - file.progressBar?.dispose(); - this.fileUploader._createFileProgressBar(file); - } - - if(file.isInitialized) { - return; - } - - file.onLoadStart.add(this._onUploadStarted.bind(this, file)); - file.onLoad.add(this._onLoadedHandler.bind(this, file)); - file.onError.add(this._onErrorHandler.bind(this, file)); - file.onAbort.add(this._onAbortHandler.bind(this, file)); - file.onProgress.add(this._onProgressHandler.bind(this, file)); - file.isInitialized = true; - } - - _shouldHandleError(file, e) { - return (this._isStatusError(e.status) || !file._isProgressStarted) && !file.isAborted; + if (file.webkitSlice) { + return file.webkitSlice(startPos, startPos + length); } + return null; + } +} - _isStatusError(status) { - return 400 <= status && status < 500 || 500 <= status && status < 600; - } +class FileUploadStrategyBase { + fileUploader?: any; - _onUploadStarted(file, e) { - file.uploadStarted = true; + constructor(fileUploader) { + this.fileUploader = fileUploader; + } - this.fileUploader._uploadStartedAction({ - file: file.value, - event: e, - request: file.request - }); + upload(file) { + if (file.isInitialized && file.isAborted) { + this.fileUploader._resetFileState(file); } - - _onAbortHandler(file, e) { - const args = { - file: file.value, - event: e, - request: file.request, - message: this.fileUploader._getUploadAbortedStatusMessage() - }; - this.fileUploader._uploadAbortedAction(args); - this.fileUploader._setStatusMessage(file, args.message); - this.fileUploader._handleAllFilesUploaded(); - } - - _onErrorHandler(file, error) { - const args = { - file: file.value, - event: undefined, - request: file.request, - error, - message: this.fileUploader.option('uploadFailedMessage') - }; - this.fileUploader._uploadErrorAction(args); - this.fileUploader._setStatusMessage(file, args.message); - this.fileUploader._handleAllFilesUploaded(); - } - - _onLoadedHandler(file, e) { - const args = { - file: file.value, - event: e, - request: file.request, - message: this.fileUploader.option('uploadedMessage') - }; - file._isLoaded = true; - this.fileUploader._uploadedAction(args); - this.fileUploader._setStatusMessage(file, args.message); - this.fileUploader._handleAllFilesUploaded(); - } - - _onProgressHandler(file, e) { - if(file) { - const totalFilesSize = this.fileUploader._getTotalFilesSize(); - const totalLoadedFilesSize = this.fileUploader._getTotalLoadedFilesSize(); - - const loadedSize = Math.min(e.loaded, file.value.size); - const segmentSize = loadedSize - file.loadedSize; - file.loadedSize = loadedSize; - - this.fileUploader._updateTotalProgress(totalFilesSize, totalLoadedFilesSize + segmentSize); - this.fileUploader._updateProgressBar(file, this._getLoadedData(loadedSize, e.total, segmentSize, e)); - } - } - - _getLoadedData(loaded, total, currentSegmentSize, event) { - return { - loaded: loaded, - total: total, - currentSegmentSize: currentSegmentSize - }; - } - - _extendFormData(formData) { - const formDataEntries = this.fileUploader.option('uploadCustomData'); - for(const entryName in formDataEntries) { - if(Object.prototype.hasOwnProperty.call(formDataEntries, entryName) && isDefined(formDataEntries[entryName])) { - formData.append(entryName, formDataEntries[entryName]); - } - } + if (file.isValid() && !file.uploadStarted) { + this._prepareFileBeforeUpload(file); + this._uploadCore(file); } + } + + abortUpload(file) { + if (file._isError || file._isLoaded || file.isAborted || !file.uploadStarted) { + return; + } + + file.isAborted = true; + file.request && file.request.abort(); + + if (this._isCustomCallback('abortUpload')) { + const abortUpload = this.fileUploader.option('abortUpload'); + const arg = this._createUploadArgument(file); + + let deferred = null; + try { + const result = abortUpload(file.value, arg); + deferred = fromPromise(result); + } catch (error) { + // @ts-expect-error + deferred = Deferred().reject(error).promise(); + } + // @ts-expect-error + deferred + .done(() => file.onAbort.fire()) + .fail((error) => this._handleFileError(file, error)); + } + } + + _beforeSend(xhr, file) { + const arg = this._createUploadArgument(file); + this.fileUploader._beforeSendAction({ + request: xhr, + file: file.value, + uploadInfo: arg, + }); + file.request = xhr; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _createUploadArgument(file) { + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _uploadCore(file) { + } + + _isCustomCallback(name) { + const callback = this.fileUploader.option(name); + return callback && isFunction(callback); + } + + _handleProgress(file, e) { + if (file._isError) { + return; + } + + file._isProgressStarted = true; + this._handleProgressCore(file, e); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _handleProgressCore(file, e) { + } + + _handleFileError(file, error) { + file._isError = true; + file.onError.fire(error); + } + + _prepareFileBeforeUpload(file) { + if (file.$file) { + file.progressBar?.dispose(); + this.fileUploader._createFileProgressBar(file); + } + + if (file.isInitialized) { + return; + } + + file.onLoadStart.add(this._onUploadStarted.bind(this, file)); + file.onLoad.add(this._onLoadedHandler.bind(this, file)); + file.onError.add(this._onErrorHandler.bind(this, file)); + file.onAbort.add(this._onAbortHandler.bind(this, file)); + file.onProgress.add(this._onProgressHandler.bind(this, file)); + file.isInitialized = true; + } + + _shouldHandleError(file, e) { + return (this._isStatusError(e.status) || !file._isProgressStarted) && !file.isAborted; + } + + _isStatusError(status) { + return status >= 400 && status < 500 || status >= 500 && status < 600; + } + + _onUploadStarted(file, e) { + file.uploadStarted = true; + + this.fileUploader._uploadStartedAction({ + file: file.value, + event: e, + request: file.request, + }); + } + + _onAbortHandler(file, e) { + const args = { + file: file.value, + event: e, + request: file.request, + message: this.fileUploader._getUploadAbortedStatusMessage(), + }; + this.fileUploader._uploadAbortedAction(args); + this.fileUploader._setStatusMessage(file, args.message); + this.fileUploader._handleAllFilesUploaded(); + } + + _onErrorHandler(file, error) { + const args = { + file: file.value, + event: undefined, + request: file.request, + error, + message: this.fileUploader.option('uploadFailedMessage'), + }; + this.fileUploader._uploadErrorAction(args); + this.fileUploader._setStatusMessage(file, args.message); + this.fileUploader._handleAllFilesUploaded(); + } + + _onLoadedHandler(file, e) { + const args = { + file: file.value, + event: e, + request: file.request, + message: this.fileUploader.option('uploadedMessage'), + }; + file._isLoaded = true; + this.fileUploader._uploadedAction(args); + this.fileUploader._setStatusMessage(file, args.message); + this.fileUploader._handleAllFilesUploaded(); + } + + _onProgressHandler(file, e) { + if (file) { + const totalFilesSize = this.fileUploader._getTotalFilesSize(); + const totalLoadedFilesSize = this.fileUploader._getTotalLoadedFilesSize(); + + const loadedSize = Math.min(e.loaded, file.value.size); + const segmentSize = loadedSize - file.loadedSize; + file.loadedSize = loadedSize; + + this.fileUploader._updateTotalProgress(totalFilesSize, totalLoadedFilesSize + segmentSize); + this.fileUploader._updateProgressBar(file, this._getLoadedData(loadedSize, e.total, segmentSize, e)); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _getLoadedData(loaded, total, currentSegmentSize, event) { + return { + loaded, + total, + currentSegmentSize, + }; + } + + _extendFormData(formData) { + const formDataEntries = this.fileUploader.option('uploadCustomData'); + // eslint-disable-next-line no-restricted-syntax + for (const entryName in formDataEntries) { + if (Object.prototype.hasOwnProperty.call(formDataEntries, entryName) && isDefined(formDataEntries[entryName])) { + formData.append(entryName, formDataEntries[entryName]); + } + } + } } class ChunksFileUploadStrategyBase extends FileUploadStrategyBase { - constructor(fileUploader) { - super(fileUploader); - this.chunkSize = this.fileUploader.option('chunkSize'); - } - - _uploadCore(file) { - const realFile = file.value; - const chunksData = { - name: realFile.name, - loadedBytes: 0, - type: realFile.type, - blobReader: new FileBlobReader(realFile, this.chunkSize), - guid: new Guid(), - fileSize: realFile.size, - count: this._getFileChunksCount(realFile), - customData: {} - }; - file.chunksData = chunksData; - this._sendChunk(file, chunksData); - } - - _getFileChunksCount(jsFile) { - return jsFile.size === 0 - ? 1 - : Math.ceil(jsFile.size / this.chunkSize); - } - - _sendChunk(file, chunksData) { - const chunk = chunksData.blobReader.read(); - chunksData.currentChunk = chunk; - if(chunk) { - this._sendChunkCore(file, chunksData, chunk) - .done(() => { - if(file.isAborted) { - return; - } - - chunksData.loadedBytes += chunk.blob.size; - - file.onProgress.fire({ - loaded: chunksData.loadedBytes, - total: file.value.size - }); - - if(chunk.isCompleted) { - file.onLoad.fire(); - } - - setTimeout(() => this._sendChunk(file, chunksData)); - }) - .fail(error => { - if(this._shouldHandleError(file, error)) { - this._handleFileError(file, error); - } - }); - } - } - - _sendChunkCore(file, chunksData, chunk) { - } + chunkSize?: any; + + constructor(fileUploader) { + super(fileUploader); + this.chunkSize = this.fileUploader.option('chunkSize'); + } + + _uploadCore(file) { + const realFile = file.value; + const chunksData = { + name: realFile.name, + loadedBytes: 0, + type: realFile.type, + blobReader: new FileBlobReader(realFile, this.chunkSize), + guid: new Guid(), + fileSize: realFile.size, + count: this._getFileChunksCount(realFile), + customData: {}, + }; + file.chunksData = chunksData; + this._sendChunk(file, chunksData); + } + + _getFileChunksCount(jsFile) { + return jsFile.size === 0 + ? 1 + : Math.ceil(jsFile.size / this.chunkSize); + } + + _sendChunk(file, chunksData) { + const chunk = chunksData.blobReader.read(); + chunksData.currentChunk = chunk; + if (chunk) { + this._sendChunkCore(file, chunksData, chunk) + .done(() => { + if (file.isAborted) { + return; + } - _tryRaiseStartLoad(file) { - if(!file.isStartLoad) { - file.isStartLoad = true; - file.onLoadStart.fire(); - } - } + chunksData.loadedBytes += chunk.blob.size; - _getEvent(e) { - return null; - } + file.onProgress.fire({ + loaded: chunksData.loadedBytes, + total: file.value.size, + }); - _createUploadArgument(file) { - return this._createChunksInfo(file.chunksData); - } + if (chunk.isCompleted) { + file.onLoad.fire(); + } - _createChunksInfo(chunksData) { - return { - bytesUploaded: chunksData.loadedBytes, - chunkCount: chunksData.count, - customData: chunksData.customData, - chunkBlob: chunksData.currentChunk.blob, - chunkIndex: chunksData.currentChunk.index - }; + setTimeout(() => this._sendChunk(file, chunksData)); + }) + .fail((error) => { + if (this._shouldHandleError(file, error)) { + this._handleFileError(file, error); + } + }); } - + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _sendChunkCore(file, chunksData, chunk): any { + } + + _tryRaiseStartLoad(file) { + if (!file.isStartLoad) { + file.isStartLoad = true; + file.onLoadStart.fire(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _getEvent(e) { + return null; + } + + _createUploadArgument(file) { + return this._createChunksInfo(file.chunksData); + } + + _createChunksInfo(chunksData) { + return { + bytesUploaded: chunksData.loadedBytes, + chunkCount: chunksData.count, + customData: chunksData.customData, + chunkBlob: chunksData.currentChunk.blob, + chunkIndex: chunksData.currentChunk.index, + }; + } } class DefaultChunksFileUploadStrategy extends ChunksFileUploadStrategyBase { - - _sendChunkCore(file, chunksData, chunk) { - return ajax.sendRequest({ - url: this.fileUploader.option('uploadUrl'), - method: this.fileUploader.option('uploadMethod'), - headers: this.fileUploader.option('uploadHeaders'), - beforeSend: xhr => this._beforeSend(xhr, file), - upload: { - 'onprogress': e => this._handleProgress(file, e), - 'onloadstart': () => this._tryRaiseStartLoad(file), - 'onabort': () => file.onAbort.fire() - }, - data: this._createFormData({ - fileName: chunksData.name, - blobName: this.fileUploader.option('name'), - blob: chunk.blob, - index: chunk.index, - count: chunksData.count, - type: chunksData.type, - guid: chunksData.guid, - size: chunksData.fileSize - }) - }); - } - - _createFormData(options) { - const formData = new window.FormData(); - formData.append(options.blobName, options.blob); - formData.append(FILEUPLOADER_CHUNK_META_DATA_NAME, JSON.stringify({ - FileName: options.fileName, - Index: options.index, - TotalCount: options.count, - FileSize: options.size, - FileType: options.type, - FileGuid: options.guid - })); - this._extendFormData(formData); - return formData; - } - + _sendChunkCore(file, chunksData, chunk) { + return ajax.sendRequest({ + url: this.fileUploader.option('uploadUrl'), + method: this.fileUploader.option('uploadMethod'), + headers: this.fileUploader.option('uploadHeaders'), + beforeSend: (xhr) => this._beforeSend(xhr, file), + upload: { + onprogress: (e) => this._handleProgress(file, e), + onloadstart: () => this._tryRaiseStartLoad(file), + onabort: () => file.onAbort.fire(), + }, + data: this._createFormData({ + fileName: chunksData.name, + blobName: this.fileUploader.option('name'), + blob: chunk.blob, + index: chunk.index, + count: chunksData.count, + type: chunksData.type, + guid: chunksData.guid, + size: chunksData.fileSize, + }), + }); + } + + _createFormData(options) { + // @ts-expect-error + const formData = new window.FormData(); + formData.append(options.blobName, options.blob); + formData.append(FILEUPLOADER_CHUNK_META_DATA_NAME, JSON.stringify({ + FileName: options.fileName, + Index: options.index, + TotalCount: options.count, + FileSize: options.size, + FileType: options.type, + FileGuid: options.guid, + })); + this._extendFormData(formData); + return formData; + } } class CustomChunksFileUploadStrategy extends ChunksFileUploadStrategyBase { - - _sendChunkCore(file, chunksData) { - this._tryRaiseStartLoad(file); - - const chunksInfo = this._createChunksInfo(chunksData); - const uploadChunk = this.fileUploader.option('uploadChunk'); - try { - const result = uploadChunk(file.value, chunksInfo); - return fromPromise(result); - } catch(error) { - return new Deferred().reject(error).promise(); - } - } - - _shouldHandleError(file, error) { - return true; - } + _sendChunkCore(file, chunksData) { + this._tryRaiseStartLoad(file); + + const chunksInfo = this._createChunksInfo(chunksData); + const uploadChunk = this.fileUploader.option('uploadChunk'); + try { + const result = uploadChunk(file.value, chunksInfo); + return fromPromise(result); + } catch (error) { + return Deferred().reject(error).promise(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _shouldHandleError(file, error) { + return true; + } } class WholeFileUploadStrategyBase extends FileUploadStrategyBase { - - _uploadCore(file) { - file.loadedSize = 0; - this._uploadFile(file) - .done(() => { - if(!file.isAborted) { - file.onLoad.fire(); - } - }) - .fail(error => { - if(this._shouldHandleError(file, error)) { - this._handleFileError(file, error); - } - }); - } - - _uploadFile(file) { - } - - _handleProgressCore(file, e) { - file.onProgress.fire(e); - } - - _getLoadedData(loaded, total, segmentSize, event) { - const result = super._getLoadedData(loaded, total, segmentSize, event); - result.event = event; - return result; - } + _uploadCore(file) { + file.loadedSize = 0; + this._uploadFile(file) + .done(() => { + if (!file.isAborted) { + file.onLoad.fire(); + } + }) + .fail((error) => { + if (this._shouldHandleError(file, error)) { + this._handleFileError(file, error); + } + }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _uploadFile(file): any { + } + + _handleProgressCore(file, e) { + file.onProgress.fire(e); + } + + _getLoadedData(loaded, total, segmentSize, event) { + const result = super._getLoadedData(loaded, total, segmentSize, event); + // @ts-expect-error + result.event = event; + return result; + } } class DefaultWholeFileUploadStrategy extends WholeFileUploadStrategyBase { - - _uploadFile(file) { - return ajax.sendRequest({ - url: this.fileUploader.option('uploadUrl'), - method: this.fileUploader.option('uploadMethod'), - headers: this.fileUploader.option('uploadHeaders'), - beforeSend: xhr => this._beforeSend(xhr, file), - upload: { - 'onprogress': e => this._handleProgress(file, e), - 'onloadstart': () => file.onLoadStart.fire(), - 'onabort': () => file.onAbort.fire() - }, - data: this._createFormData(this.fileUploader.option('name'), file.value) - }); - } - - _createFormData(fieldName, fieldValue) { - const formData = new window.FormData(); - formData.append(fieldName, fieldValue, fieldValue.name); - this._extendFormData(formData); - return formData; - } - + _uploadFile(file) { + return ajax.sendRequest({ + url: this.fileUploader.option('uploadUrl'), + method: this.fileUploader.option('uploadMethod'), + headers: this.fileUploader.option('uploadHeaders'), + beforeSend: (xhr) => this._beforeSend(xhr, file), + upload: { + onprogress: (e) => this._handleProgress(file, e), + onloadstart: () => file.onLoadStart.fire(), + onabort: () => file.onAbort.fire(), + }, + data: this._createFormData(this.fileUploader.option('name'), file.value), + }); + } + + _createFormData(fieldName, fieldValue) { + // @ts-expect-error + const formData = new window.FormData(); + formData.append(fieldName, fieldValue, fieldValue.name); + this._extendFormData(formData); + return formData; + } } class CustomWholeFileUploadStrategy extends WholeFileUploadStrategyBase { - - _uploadFile(file) { - file.onLoadStart.fire(); - - const progressCallback = loadedBytes => { - const arg = { - loaded: loadedBytes, - total: file.value.size - }; - this._handleProgress(file, arg); - }; - - const uploadFile = this.fileUploader.option('uploadFile'); - try { - const result = uploadFile(file.value, progressCallback); - return fromPromise(result); - } catch(error) { - return new Deferred().reject(error).promise(); - } - } - - _shouldHandleError(file, e) { - return true; - } - + _uploadFile(file) { + file.onLoadStart.fire(); + + const progressCallback = (loadedBytes) => { + const arg = { + loaded: loadedBytes, + total: file.value.size, + }; + this._handleProgress(file, arg); + }; + + const uploadFile = this.fileUploader.option('uploadFile'); + try { + const result = uploadFile(file.value, progressCallback); + return fromPromise(result); + } catch (error) { + return Deferred().reject(error).promise(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _shouldHandleError(file, e) { + return true; + } } - +// @ts-expect-error registerComponent('dxFileUploader', FileUploader); export default FileUploader; diff --git a/packages/devextreme/js/__internal/ui/menu/m_menu.ts b/packages/devextreme/js/__internal/ui/menu/m_menu.ts index 0305fbf68e34..8128f2b4aae0 100644 --- a/packages/devextreme/js/__internal/ui/menu/m_menu.ts +++ b/packages/devextreme/js/__internal/ui/menu/m_menu.ts @@ -13,7 +13,7 @@ import { end as hoverEventEnd } from '@js/events/hover'; import pointerEvents from '@js/events/pointer'; import { addNamespace } from '@js/events/utils/index'; import Button from '@js/ui/button'; -import type { Item, Properties } from '@js/ui/menu'; +import type { Item } from '@js/ui/menu'; import type { Properties as OverlayProperties } from '@js/ui/overlay'; import type dxOverlay from '@js/ui/overlay'; import Overlay from '@js/ui/overlay/ui.overlay'; @@ -80,8 +80,6 @@ class Menu extends MenuBase { _actions?: any; - _optionsByReference?: Properties; - _showSubmenuTimer?: any; _hideSubmenuTimer?: any; diff --git a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts index 5a1249004ffa..6cd1187c3561 100644 --- a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts +++ b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts @@ -234,7 +234,6 @@ class RadioGroup extends Editor { _setOptionsByReference(): void { super._setOptionsByReference(); - // @ts-expect-error extend(this._optionsByReference, { value: true }); } diff --git a/packages/devextreme/js/__internal/ui/widget.ts b/packages/devextreme/js/__internal/ui/widget.ts index d5079909696f..c109e02217e6 100644 --- a/packages/devextreme/js/__internal/ui/widget.ts +++ b/packages/devextreme/js/__internal/ui/widget.ts @@ -16,6 +16,8 @@ declare class ExtendedWidget extends Widget { // component _deprecatedOptions: Record; + _optionsByReference: Record; + setAria(ariaOptions: AriaOptions, $element?: dxElementWrapper): void; setAria( attribute: string, @@ -64,6 +66,7 @@ declare class ExtendedWidget extends Widget { // component _init(): void; + _initOptions(options: TProperties): void; _createActionByOption(optionName: string, config?: Record); _isInitialOptionValue(name: string): boolean; _setDeprecatedOptions(): void; diff --git a/packages/devextreme/js/ui/file_uploader.js b/packages/devextreme/js/ui/file_uploader.js new file mode 100644 index 000000000000..29f76ccce87c --- /dev/null +++ b/packages/devextreme/js/ui/file_uploader.js @@ -0,0 +1,22 @@ +import FileUploader from '../__internal/ui/m_file_uploader'; + +export default FileUploader; + +// STYLE fileUploader + +/** + * @name dxFileUploaderOptions.extendSelection + * @type boolean + * @default true + * @hidden + */ + +/** + * @name dxFileUploaderOptions.validationMessageMode + * @hidden + */ + +/** + * @name dxFileUploaderOptions.validationMessagePosition + * @hidden + */ From 8230457eede19a7821fb26302fb1751d7682d6be Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 11 Jun 2024 18:46:12 +0400 Subject: [PATCH 11/32] NumberBox: move files to TS --- .../ui/number_box/m_number_box.base.ts} | 0 .../ui/number_box/m_number_box.caret.ts} | 0 .../ui/number_box/m_number_box.mask.ts} | 0 .../ui/number_box/m_number_box.spin.ts} | 0 .../ui/number_box/m_number_box.spins.ts} | 0 .../number_box.js => __internal/ui/number_box/m_number_box.ts} | 0 .../number_box/utils.js => __internal/ui/number_box/m_utils.ts} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/number_box/number_box.base.js => __internal/ui/number_box/m_number_box.base.ts} (100%) rename packages/devextreme/js/{ui/number_box/number_box.caret.js => __internal/ui/number_box/m_number_box.caret.ts} (100%) rename packages/devextreme/js/{ui/number_box/number_box.mask.js => __internal/ui/number_box/m_number_box.mask.ts} (100%) rename packages/devextreme/js/{ui/number_box/number_box.spin.js => __internal/ui/number_box/m_number_box.spin.ts} (100%) rename packages/devextreme/js/{ui/number_box/number_box.spins.js => __internal/ui/number_box/m_number_box.spins.ts} (100%) rename packages/devextreme/js/{ui/number_box/number_box.js => __internal/ui/number_box/m_number_box.ts} (100%) rename packages/devextreme/js/{ui/number_box/utils.js => __internal/ui/number_box/m_utils.ts} (100%) diff --git a/packages/devextreme/js/ui/number_box/number_box.base.js b/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts similarity index 100% rename from packages/devextreme/js/ui/number_box/number_box.base.js rename to packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts diff --git a/packages/devextreme/js/ui/number_box/number_box.caret.js b/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts similarity index 100% rename from packages/devextreme/js/ui/number_box/number_box.caret.js rename to packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts diff --git a/packages/devextreme/js/ui/number_box/number_box.mask.js b/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts similarity index 100% rename from packages/devextreme/js/ui/number_box/number_box.mask.js rename to packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts diff --git a/packages/devextreme/js/ui/number_box/number_box.spin.js b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts similarity index 100% rename from packages/devextreme/js/ui/number_box/number_box.spin.js rename to packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts diff --git a/packages/devextreme/js/ui/number_box/number_box.spins.js b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts similarity index 100% rename from packages/devextreme/js/ui/number_box/number_box.spins.js rename to packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts diff --git a/packages/devextreme/js/ui/number_box/number_box.js b/packages/devextreme/js/__internal/ui/number_box/m_number_box.ts similarity index 100% rename from packages/devextreme/js/ui/number_box/number_box.js rename to packages/devextreme/js/__internal/ui/number_box/m_number_box.ts diff --git a/packages/devextreme/js/ui/number_box/utils.js b/packages/devextreme/js/__internal/ui/number_box/m_utils.ts similarity index 100% rename from packages/devextreme/js/ui/number_box/utils.js rename to packages/devextreme/js/__internal/ui/number_box/m_utils.ts From 0fbfc03059dc2d8136f68ad67875895c6c94cd36 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 11 Jun 2024 19:09:13 +0400 Subject: [PATCH 12/32] NumberBox: ignore errors after move to TS --- .../ui/number_box/m_number_box.base.ts | 831 +++++----- .../ui/number_box/m_number_box.caret.ts | 203 +-- .../ui/number_box/m_number_box.mask.ts | 1332 +++++++++-------- .../ui/number_box/m_number_box.spin.ts | 184 +-- .../ui/number_box/m_number_box.spins.ts | 198 +-- .../__internal/ui/number_box/m_number_box.ts | 5 +- .../js/__internal/ui/number_box/m_utils.ts | 74 +- packages/devextreme/js/ui/number_box.js | 40 +- .../numberBoxParts/common.tests.js | 2 +- .../numberBoxParts/mask.caret.tests.js | 100 +- .../rangeSlider.tests.js | 4 +- 11 files changed, 1501 insertions(+), 1472 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts index 6c730fe160ab..00f836ade6c6 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts @@ -1,17 +1,24 @@ -import $ from '../../core/renderer'; -import domAdapter from '../../core/dom_adapter'; -import eventsEngine from '../../events/core/events_engine'; -import { applyServerDecimalSeparator, ensureDefined } from '../../core/utils/common'; -import { isDefined } from '../../core/utils/type'; -import { fitIntoRange, inRange } from '../../core/utils/math'; -import { extend } from '../../core/utils/extend'; -import devices from '../../core/devices'; -import browser from '../../core/utils/browser'; -import TextEditor from '../text_box/ui.text_editor'; -import { addNamespace, getChar, isCommandKeyPressed, normalizeKeyName } from '../../events/utils/index'; -import SpinButtons from './number_box.spins'; -import messageLocalization from '../../localization/message'; -import { Deferred } from '../../core/utils/deferred'; +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import $ from '@js/core/renderer'; +import browser from '@js/core/utils/browser'; +import { + // @ts-expect-error + applyServerDecimalSeparator, + ensureDefined, +} from '@js/core/utils/common'; +import { Deferred } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { fitIntoRange, inRange } from '@js/core/utils/math'; +import { isDefined } from '@js/core/utils/type'; +import eventsEngine from '@js/events/core/events_engine'; +import { + addNamespace, getChar, isCommandKeyPressed, normalizeKeyName, +} from '@js/events/utils/index'; +import messageLocalization from '@js/localization/message'; +import TextEditor from '@js/ui/text_box/ui.text_editor'; + +import SpinButtons from './m_number_box.spins'; const math = Math; @@ -22,479 +29,445 @@ const FORCE_VALUECHANGE_EVENT_NAMESPACE = 'NumberBoxForceValueChange'; const NumberBoxBase = TextEditor.inherit({ - _supportedKeys: function() { - return extend(this.callBase(), { - upArrow: function(e) { - if(!isCommandKeyPressed(e)) { - e.preventDefault(); - e.stopPropagation(); - this._spinUpChangeHandler(e); - } - }, - downArrow: function(e) { - if(!isCommandKeyPressed(e)) { - e.preventDefault(); - e.stopPropagation(); - this._spinDownChangeHandler(e); - } - }, - enter: function() { - } - }); - }, - - _getDefaultOptions: function() { - return extend(this.callBase(), { - value: 0, - - min: undefined, - - max: undefined, - - step: 1, - - showSpinButtons: false, - - useLargeSpinButtons: true, - - mode: 'text', - - invalidValueMessage: messageLocalization.format('dxNumberBox-invalidValueMessage'), - - buttons: void 0, - - /** - * @name dxNumberBoxOptions.mask - * @hidden - */ - - /** - * @name dxNumberBoxOptions.maskChar - * @hidden - */ - - /** - * @name dxNumberBoxOptions.maskRules - * @hidden - */ - - /** - * @name dxNumberBoxOptions.maskInvalidMessage - * @hidden - */ - - /** - * @name dxNumberBoxOptions.useMaskedValue - * @hidden - */ - - /** - * @name dxNumberBoxOptions.showMaskMode - * @hidden - */ - - /** - * @name dxNumberBoxOptions.spellcheck - * @hidden - */ - }); - }, - - _useTemplates: function() { - return false; - }, - - _getDefaultButtons: function() { - return this.callBase().concat([{ name: 'spins', Ctor: SpinButtons }]); - }, - - _isSupportInputMode: function() { - const version = parseFloat(browser.version); - - return ( - browser.chrome && version >= 66 - || browser.safari && version >= 12 - ); - }, - - _defaultOptionsRules: function() { - return this.callBase().concat([ - { - device: function() { - return devices.real().generic && !devices.isSimulator(); - }, - options: { - useLargeSpinButtons: false - } - }, - { - device: function() { - return devices.real().deviceType !== 'desktop' && !this._isSupportInputMode(); - }.bind(this), - options: { - mode: 'number' - } - } - ]); - }, - - _initMarkup: function() { - this._renderSubmitElement(); - this.$element().addClass(WIDGET_CLASS); - - this.callBase(); - }, - - _getDefaultAttributes: function() { - const attributes = this.callBase(); - - attributes['inputmode'] = 'decimal'; - return attributes; - }, - - _renderContentImpl: function() { - this.option('isValid') && this._validateValue(this.option('value')); - this.setAria('role', 'spinbutton'); - }, - - _renderSubmitElement: function() { - this._$submitElement = $('') - .attr('type', 'hidden') - .appendTo(this.$element()); - this._setSubmitValue(this.option('value')); - }, - - _setSubmitValue: function(value) { - this._getSubmitElement().val(applyServerDecimalSeparator(value)); - }, - - _getSubmitElement: function() { - return this._$submitElement; - }, - - _keyPressHandler: function(e) { - this.callBase(e); - - const char = getChar(e); - const validCharRegExp = /[\d.,eE\-+]/; - const isInputCharValid = validCharRegExp.test(char); - - if(!isInputCharValid) { - const keyName = normalizeKeyName(e); - // NOTE: Additional check for Firefox control keys - if(isCommandKeyPressed(e) || keyName && FIREFOX_CONTROL_KEYS.includes(keyName)) { - return; - } - - e.preventDefault(); - return false; + _supportedKeys() { + return extend(this.callBase(), { + upArrow(e) { + if (!isCommandKeyPressed(e)) { + e.preventDefault(); + e.stopPropagation(); + this._spinUpChangeHandler(e); } - - this._keyPressed = true; - }, - - _onMouseWheel: function(dxEvent) { - dxEvent.delta > 0 ? this._spinValueChange(1, dxEvent) : this._spinValueChange(-1, dxEvent); - }, - - _renderValue: function() { - const inputValue = this._input().val(); - const value = this.option('value'); - - if(!inputValue.length || Number(inputValue) !== value) { - this._forceValueRender(); - this._toggleEmptinessEventHandler(); + }, + downArrow(e) { + if (!isCommandKeyPressed(e)) { + e.preventDefault(); + e.stopPropagation(); + this._spinDownChangeHandler(e); } + }, + enter() { + }, + }); + }, + + _getDefaultOptions() { + return extend(this.callBase(), { + value: 0, + min: undefined, + max: undefined, + step: 1, + showSpinButtons: false, + useLargeSpinButtons: true, + mode: 'text', + invalidValueMessage: messageLocalization.format('dxNumberBox-invalidValueMessage'), + // eslint-disable-next-line no-void + buttons: void 0, + }); + }, + + _useTemplates() { + return false; + }, + + _getDefaultButtons() { + return this.callBase().concat([{ name: 'spins', Ctor: SpinButtons }]); + }, + + _isSupportInputMode() { + // @ts-expect-error + const version = parseFloat(browser.version); + + return ( + browser.chrome && version >= 66 + || browser.safari && version >= 12 + ); + }, + + _defaultOptionsRules() { + return this.callBase().concat([ + { + device() { + return devices.real().generic && !devices.isSimulator(); + }, + options: { + useLargeSpinButtons: false, + }, + }, + { + device: function () { + return devices.real().deviceType !== 'desktop' && !this._isSupportInputMode(); + }.bind(this), + options: { + mode: 'number', + }, + }, + ]); + }, + + _initMarkup() { + this._renderSubmitElement(); + this.$element().addClass(WIDGET_CLASS); + + this.callBase(); + }, + + _getDefaultAttributes() { + const attributes = this.callBase(); + // eslint-disable-next-line spellcheck/spell-checker + attributes.inputmode = 'decimal'; + return attributes; + }, + + _renderContentImpl() { + this.option('isValid') && this._validateValue(this.option('value')); + this.setAria('role', 'spinbutton'); + }, + + _renderSubmitElement() { + this._$submitElement = $('') + .attr('type', 'hidden') + .appendTo(this.$element()); + this._setSubmitValue(this.option('value')); + }, + + _setSubmitValue(value) { + this._getSubmitElement().val(applyServerDecimalSeparator(value)); + }, + + _getSubmitElement() { + return this._$submitElement; + }, + // @ts-expect-error + _keyPressHandler(e) { + this.callBase(e); + + const char = getChar(e); + const validCharRegExp = /[\d.,eE\-+]/; + const isInputCharValid = validCharRegExp.test(char); + + if (!isInputCharValid) { + const keyName = normalizeKeyName(e); + // NOTE: Additional check for Firefox control keys + if (isCommandKeyPressed(e) || keyName && FIREFOX_CONTROL_KEYS.includes(keyName)) { + return; + } + + e.preventDefault(); + return false; + } - const valueText = isDefined(value) ? null : messageLocalization.format('dxNumberBox-noDataText'); - - this.setAria({ - 'valuenow': ensureDefined(value, ''), - 'valuetext': valueText - }); - - this.option('text', this._input().val()); - this._updateButtons(); - - return new Deferred().resolve(); - }, - - _forceValueRender: function() { - const value = this.option('value'); - const number = Number(value); - const formattedValue = isNaN(number) ? '' : this._applyDisplayValueFormatter(value); - - this._renderDisplayText(formattedValue); - }, + this._keyPressed = true; + }, - _applyDisplayValueFormatter: function(value) { - return this.option('displayValueFormatter')(value); - }, + _onMouseWheel(dxEvent) { + dxEvent.delta > 0 ? this._spinValueChange(1, dxEvent) : this._spinValueChange(-1, dxEvent); + }, - _renderProps: function() { - this._input().prop({ - 'min': this.option('min'), - 'max': this.option('max'), - 'step': this.option('step') - }); + _renderValue() { + const inputValue = this._input().val(); + const value = this.option('value'); - this.setAria({ - 'valuemin': ensureDefined(this.option('min'), ''), - 'valuemax': ensureDefined(this.option('max'), '') - }); - }, + if (!inputValue.length || Number(inputValue) !== value) { + this._forceValueRender(); + this._toggleEmptinessEventHandler(); + } - _spinButtonsPointerDownHandler: function() { - const $input = this._input(); - if(!this.option('useLargeSpinButtons') && domAdapter.getActiveElement() !== $input[0]) { - eventsEngine.trigger($input, 'focus'); - } - }, + const valueText = isDefined(value) ? null : messageLocalization.format('dxNumberBox-noDataText'); + + this.setAria({ + // eslint-disable-next-line spellcheck/spell-checker + valuenow: ensureDefined(value, ''), + // eslint-disable-next-line spellcheck/spell-checker + valuetext: valueText, + }); + + this.option('text', this._input().val()); + this._updateButtons(); + + return Deferred().resolve(); + }, + + _forceValueRender() { + const value = this.option('value'); + const number = Number(value); + const formattedValue = isNaN(number) ? '' : this._applyDisplayValueFormatter(value); + + this._renderDisplayText(formattedValue); + }, + + _applyDisplayValueFormatter(value) { + return this.option('displayValueFormatter')(value); + }, + + _renderProps() { + this._input().prop({ + min: this.option('min'), + max: this.option('max'), + step: this.option('step'), + }); + + this.setAria({ + // eslint-disable-next-line spellcheck/spell-checker + valuemin: ensureDefined(this.option('min'), ''), + // eslint-disable-next-line spellcheck/spell-checker + valuemax: ensureDefined(this.option('max'), ''), + }); + }, + + _spinButtonsPointerDownHandler() { + const $input = this._input(); + if (!this.option('useLargeSpinButtons') && domAdapter.getActiveElement() !== $input[0]) { + // @ts-expect-error + eventsEngine.trigger($input, 'focus'); + } + }, - _spinUpChangeHandler: function(e) { - if(!this.option('readOnly')) { - this._spinValueChange(1, e.event || e); - } - }, + _spinUpChangeHandler(e) { + if (!this.option('readOnly')) { + this._spinValueChange(1, e.event || e); + } + }, - _spinDownChangeHandler: function(e) { - if(!this.option('readOnly')) { - this._spinValueChange(-1, e.event || e); - } - }, + _spinDownChangeHandler(e) { + if (!this.option('readOnly')) { + this._spinValueChange(-1, e.event || e); + } + }, - _spinValueChange: function(sign, dxEvent) { - const step = parseFloat(this.option('step')); - if(step === 0) { - return; - } + _spinValueChange(sign, dxEvent) { + const step = parseFloat(this.option('step')); + if (step === 0) { + return; + } - let value = parseFloat(this._normalizeInputValue()) || 0; + let value = parseFloat(this._normalizeInputValue()) || 0; - value = this._correctRounding(value, step * sign); + value = this._correctRounding(value, step * sign); - const min = this.option('min'); - const max = this.option('max'); + const min = this.option('min'); + const max = this.option('max'); - if(isDefined(min)) { - value = Math.max(min, value); - } + if (isDefined(min)) { + value = Math.max(min, value); + } - if(isDefined(max)) { - value = Math.min(max, value); - } + if (isDefined(max)) { + value = Math.min(max, value); + } - this._saveValueChangeEvent(dxEvent); - this.option('value', value); - }, + this._saveValueChangeEvent(dxEvent); + this.option('value', value); + }, - _correctRounding: function(value, step) { - const regex = /[,.](.*)/; - const isFloatValue = regex.test(value); - const isFloatStep = regex.test(step); + _correctRounding(value, step) { + const regex = /[,.](.*)/; + const isFloatValue = regex.test(value); + const isFloatStep = regex.test(step); - if(isFloatValue || isFloatStep) { - const valueAccuracy = (isFloatValue) ? regex.exec(value)[0].length : 0; - const stepAccuracy = (isFloatStep) ? regex.exec(step)[0].length : 0; - const accuracy = math.max(valueAccuracy, stepAccuracy); + if (isFloatValue || isFloatStep) { + // @ts-expect-error + const valueAccuracy = isFloatValue ? regex.exec(value)[0].length : 0; + // @ts-expect-error + const stepAccuracy = isFloatStep ? regex.exec(step)[0].length : 0; + const accuracy = math.max(valueAccuracy, stepAccuracy); - value = this._round(value + step, accuracy); + value = this._round(value + step, accuracy); - return value; - } + return value; + } - return value + step; - }, + return value + step; + }, - _round: function(value, precision) { - precision = precision || 0; + _round(value, precision) { + precision = precision || 0; - const multiplier = Math.pow(10, precision); + const multiplier = 10 ** precision; - value *= multiplier; - value = Math.round(value) / multiplier; + value *= multiplier; + value = Math.round(value) / multiplier; - return value; - }, + return value; + }, - _renderValueChangeEvent: function() { - this.callBase(); + _renderValueChangeEvent() { + this.callBase(); - const forceValueChangeEvent = addNamespace('focusout', FORCE_VALUECHANGE_EVENT_NAMESPACE); - eventsEngine.off(this.element(), forceValueChangeEvent); - eventsEngine.on(this.element(), forceValueChangeEvent, this._forceRefreshInputValue.bind(this)); - }, + const forceValueChangeEvent = addNamespace('focusout', FORCE_VALUECHANGE_EVENT_NAMESPACE); + eventsEngine.off(this.element(), forceValueChangeEvent); + eventsEngine.on(this.element(), forceValueChangeEvent, this._forceRefreshInputValue.bind(this)); + }, - _forceRefreshInputValue: function() { - if(this.option('mode') === 'number') { - return; - } + _forceRefreshInputValue() { + if (this.option('mode') === 'number') { + return; + } - const $input = this._input(); - const formattedValue = this._applyDisplayValueFormatter(this.option('value')); + const $input = this._input(); + const formattedValue = this._applyDisplayValueFormatter(this.option('value')); - $input.val(null); - $input.val(formattedValue); - }, + $input.val(null); + $input.val(formattedValue); + }, - _valueChangeEventHandler: function(e) { - const $input = this._input(); - const inputValue = this._normalizeText(); - const value = this._parseValue(inputValue); - const valueHasDigits = inputValue !== '.' && inputValue !== '-'; + _valueChangeEventHandler(e) { + const $input = this._input(); + const inputValue = this._normalizeText(); + const value = this._parseValue(inputValue); + const valueHasDigits = inputValue !== '.' && inputValue !== '-'; - if(this._isValueValid() && !this._validateValue(value)) { - $input.val(this._applyDisplayValueFormatter(value)); - return; - } + if (this._isValueValid() && !this._validateValue(value)) { + $input.val(this._applyDisplayValueFormatter(value)); + return; + } - if(valueHasDigits) { - this.callBase(e, isNaN(value) ? null : value); - } + if (valueHasDigits) { + this.callBase(e, isNaN(value) ? null : value); + } - this._applyValueBoundaries(inputValue, value); + this._applyValueBoundaries(inputValue, value); - this.validationRequest.fire({ - value: value, - editor: this - }); - }, + this.validationRequest.fire({ + value, + editor: this, + }); + }, - _applyValueBoundaries: function(inputValue, parsedValue) { - const isValueIncomplete = this._isValueIncomplete(inputValue); - const isValueCorrect = this._isValueInRange(inputValue); + _applyValueBoundaries(inputValue, parsedValue) { + const isValueIncomplete = this._isValueIncomplete(inputValue); + const isValueCorrect = this._isValueInRange(inputValue); - if(!isValueIncomplete && !isValueCorrect && parsedValue !== null) { - if(Number(inputValue) !== parsedValue) { - this._input().val(this._applyDisplayValueFormatter(parsedValue)); - } - } - }, + if (!isValueIncomplete && !isValueCorrect && parsedValue !== null) { + if (Number(inputValue) !== parsedValue) { + this._input().val(this._applyDisplayValueFormatter(parsedValue)); + } + } + }, - _replaceCommaWithPoint: function(value) { - return value.replace(',', '.'); - }, + _replaceCommaWithPoint(value) { + return value.replace(',', '.'); + }, - _inputIsInvalid: function() { - const isNumberMode = this.option('mode') === 'number'; - const validityState = this._input().get(0).validity; + _inputIsInvalid() { + const isNumberMode = this.option('mode') === 'number'; + const validityState = this._input().get(0).validity; - return isNumberMode && validityState && validityState.badInput; - }, + return isNumberMode && validityState && validityState.badInput; + }, - _renderDisplayText: function(text) { - if(this._inputIsInvalid()) { - return; - } + _renderDisplayText(text) { + if (this._inputIsInvalid()) { + return; + } - this.callBase(text); - }, + this.callBase(text); + }, - _isValueIncomplete: function(value) { - const incompleteRegex = /(^-$)|(^-?\d*\.$)|(\d+e-?$)/i; - return incompleteRegex.test(value); - }, + _isValueIncomplete(value) { + const incompleteRegex = /(^-$)|(^-?\d*\.$)|(\d+e-?$)/i; + return incompleteRegex.test(value); + }, - _isValueInRange: function(value) { - return inRange(value, this.option('min'), this.option('max')); - }, + _isValueInRange(value) { + return inRange(value, this.option('min'), this.option('max')); + }, - _isNumber: function(value) { - return this._parseValue(value) !== null; - }, + _isNumber(value) { + return this._parseValue(value) !== null; + }, - _validateValue: function(value) { - const inputValue = this._normalizeText(); - const isValueValid = this._isValueValid(); - let isValid = true; - const isNumber = this._isNumber(inputValue); + _validateValue(value) { + const inputValue = this._normalizeText(); + const isValueValid = this._isValueValid(); + let isValid = true; + const isNumber = this._isNumber(inputValue); - if(isNaN(Number(value))) { - isValid = false; - } + if (isNaN(Number(value))) { + isValid = false; + } - if(!value && isValueValid) { - isValid = true; - } else if(!isNumber && !isValueValid) { - isValid = false; - } + if (!value && isValueValid) { + isValid = true; + } else if (!isNumber && !isValueValid) { + isValid = false; + } - this.option({ - isValid: isValid, - validationError: isValid ? null : { - editorSpecific: true, - message: this.option('invalidValueMessage') - } - }); + this.option({ + isValid, + validationError: isValid ? null : { + editorSpecific: true, + message: this.option('invalidValueMessage'), + }, + }); - return isValid; - }, + return isValid; + }, - _normalizeInputValue: function() { - return this._parseValue(this._normalizeText()); - }, + _normalizeInputValue() { + return this._parseValue(this._normalizeText()); + }, - _normalizeText: function() { - const value = this._input().val().trim(); + _normalizeText() { + const value = this._input().val().trim(); - return this._replaceCommaWithPoint(value); - }, + return this._replaceCommaWithPoint(value); + }, - _parseValue: function(value) { - const number = parseFloat(value); + _parseValue(value) { + const number = parseFloat(value); - if(isNaN(number)) { - return null; - } + if (isNaN(number)) { + return null; + } - return fitIntoRange(number, this.option('min'), this.option('max')); - }, + return fitIntoRange(number, this.option('min'), this.option('max')); + }, - _clearValue: function() { - if(this._inputIsInvalid()) { - this._input().val(''); - this._validateValue(); - } - this.callBase(); - }, - - clear: function() { - if(this.option('value') === null) { - this.option('text', ''); - if(this._input().length) { - this._renderValue(); - } - } else { - this.option('value', null); - } - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'value': - this._validateValue(args.value); - this._setSubmitValue(args.value); - this.callBase(args); - this._resumeValueChangeAction(); - break; - case 'step': - this._renderProps(); - break; - case 'min': - case 'max': - this._renderProps(); - this.option('value', this._parseValue(this.option('value'))); - break; - case 'showSpinButtons': - case 'useLargeSpinButtons': - this._updateButtons(['spins']); - break; - case 'invalidValueMessage': - break; - default: - this.callBase(args); - } + _clearValue() { + if (this._inputIsInvalid()) { + this._input().val(''); + this._validateValue(); + } + this.callBase(); + }, + + clear() { + if (this.option('value') === null) { + this.option('text', ''); + if (this._input().length) { + this._renderValue(); + } + } else { + this.option('value', null); + } + }, + + _optionChanged(args) { + switch (args.name) { + case 'value': + this._validateValue(args.value); + this._setSubmitValue(args.value); + this.callBase(args); + this._resumeValueChangeAction(); + break; + case 'step': + this._renderProps(); + break; + case 'min': + case 'max': + this._renderProps(); + this.option('value', this._parseValue(this.option('value'))); + break; + case 'showSpinButtons': + case 'useLargeSpinButtons': + this._updateButtons(['spins']); + break; + case 'invalidValueMessage': + break; + default: + this.callBase(args); } + }, }); export default NumberBoxBase; diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts index 3440f83f454c..3373be789bca 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts @@ -1,136 +1,141 @@ -import { fitIntoRange } from '../../core/utils/math'; -import { escapeRegExp } from '../../core/utils/common'; -import number from '../../localization/number'; -import { getRealSeparatorIndex, getNthOccurrence, splitByIndex } from './utils'; +import { escapeRegExp } from '@js/core/utils/common'; +import { fitIntoRange } from '@js/core/utils/math'; +import number from '@js/localization/number'; -export const getCaretBoundaries = function(text, format) { - if(typeof format === 'string') { - const signParts = format.split(';'); - const sign = number.getSign(text, format); +import { getNthOccurrence, getRealSeparatorIndex, splitByIndex } from './m_utils'; - signParts[1] = signParts[1] || '-' + signParts[0]; - format = signParts[sign < 0 ? 1 : 0]; +export const getCaretBoundaries = function (text, format) { + if (typeof format === 'string') { + const signParts = format.split(';'); + const sign = number.getSign(text, format); - const mockEscapedStubs = (str) => str.replace(/'([^']*)'/g, str => str.split('').map(() => ' ').join('').substr(2)); + signParts[1] = signParts[1] || `-${signParts[0]}`; + format = signParts[sign < 0 ? 1 : 0]; - format = mockEscapedStubs(format); + const mockEscapedStubs = (str) => str.replace(/'([^']*)'/g, (str) => str.split('').map(() => ' ').join('').substr(2)); - const prefixStubLength = /^[^#0.,]*/.exec(format)[0].length; - const postfixStubLength = /[^#0.,]*$/.exec(format)[0].length; + format = mockEscapedStubs(format); - return { - start: prefixStubLength, - end: text.length - postfixStubLength - }; - } else { - return { start: 0, end: text.length }; - } + // @ts-expect-error + const prefixStubLength = /^[^#0.,]*/.exec(format)[0].length; + // @ts-expect-error + const postfixStubLength = /[^#0.,]*$/.exec(format)[0].length; + + return { + start: prefixStubLength, + end: text.length - postfixStubLength, + }; + } + return { start: 0, end: text.length }; }; -const _getDigitCountBeforeIndex = function(index, text) { - const decimalSeparator = number.getDecimalSeparator(); - const regExp = new RegExp('[^0-9' + escapeRegExp(decimalSeparator) + ']', 'g'); - const textBeforePosition = text.slice(0, index); +// eslint-disable-next-line @typescript-eslint/naming-convention +const _getDigitCountBeforeIndex = function (index, text) { + const decimalSeparator = number.getDecimalSeparator(); + const regExp = new RegExp(`[^0-9${escapeRegExp(decimalSeparator)}]`, 'g'); + const textBeforePosition = text.slice(0, index); - return textBeforePosition.replace(regExp, '').length; + return textBeforePosition.replace(regExp, '').length; }; -const _reverseText = function(text) { - return text.split('').reverse().join(''); +// eslint-disable-next-line @typescript-eslint/naming-convention +const _reverseText = function (text) { + return text.split('').reverse().join(''); }; -const _getDigitPositionByIndex = function(digitIndex, text) { - if(!digitIndex) { - return -1; - } - - const regExp = /[0-9]/g; - let counter = 1; - let index = null; - let result = regExp.exec(text); - - while(result) { - index = result.index; - if(counter >= digitIndex) { - return index; - } - counter++; - result = regExp.exec(text); +// eslint-disable-next-line @typescript-eslint/naming-convention +const _getDigitPositionByIndex = function (digitIndex, text) { + if (!digitIndex) { + return -1; + } + + const regExp = /[0-9]/g; + let counter = 1; + let index: number | null = null; + let result = regExp.exec(text); + + while (result) { + index = result.index; + if (counter >= digitIndex) { + return index; } + counter++; + result = regExp.exec(text); + } - return index === null ? text.length : index; + return index === null ? text.length : index; }; -const _trimNonNumericCharsFromEnd = function(text) { - return text.replace(/[^0-9e]+$/, ''); +// eslint-disable-next-line @typescript-eslint/naming-convention +const _trimNonNumericCharsFromEnd = function (text) { + return text.replace(/[^0-9e]+$/, ''); }; -export const getCaretWithOffset = function(caret, offset) { - if(caret.start === undefined) { - caret = { start: caret, end: caret }; - } +export const getCaretWithOffset = function (caret, offset) { + if (caret.start === undefined) { + caret = { start: caret, end: caret }; + } - return { - start: caret.start + offset, - end: caret.end + offset - }; + return { + start: caret.start + offset, + end: caret.end + offset, + }; }; -export const getCaretAfterFormat = function(text, formatted, caret, format) { - caret = getCaretWithOffset(caret, 0); - - const point = number.getDecimalSeparator(); - const isSeparatorBasedText = isSeparatorBasedString(text); - const realSeparatorOccurrenceIndex = getRealSeparatorIndex(format).occurrence; - const pointPosition = isSeparatorBasedText ? 0 : getNthOccurrence(text, point, realSeparatorOccurrenceIndex); - const newPointPosition = getNthOccurrence(formatted, point, realSeparatorOccurrenceIndex); - const textParts = splitByIndex(text, pointPosition); - const formattedParts = splitByIndex(formatted, newPointPosition); - const isCaretOnFloat = pointPosition !== -1 && caret.start > pointPosition; - - if(isCaretOnFloat) { - const relativeIndex = caret.start - pointPosition - 1; - const digitsBefore = _getDigitCountBeforeIndex(relativeIndex, textParts[1]); - const newPosition = formattedParts[1] ? newPointPosition + 1 + _getDigitPositionByIndex(digitsBefore, formattedParts[1]) + 1 : formatted.length; - - return getCaretInBoundaries(newPosition, formatted, format); - } else { - const formattedIntPart = _trimNonNumericCharsFromEnd(formattedParts[0]); - const positionFromEnd = textParts[0].length - caret.start; - const digitsFromEnd = _getDigitCountBeforeIndex(positionFromEnd, _reverseText(textParts[0])); - const newPositionFromEnd = _getDigitPositionByIndex(digitsFromEnd, _reverseText(formattedIntPart)); - const newPositionFromBegin = formattedIntPart.length - (newPositionFromEnd + 1); - - return getCaretInBoundaries(newPositionFromBegin, formatted, format); - } +export const getCaretAfterFormat = function (text, formatted, caret, format) { + caret = getCaretWithOffset(caret, 0); + + const point = number.getDecimalSeparator(); + const isSeparatorBasedText = isSeparatorBasedString(text); + const realSeparatorOccurrenceIndex = getRealSeparatorIndex(format).occurrence; + const pointPosition = isSeparatorBasedText ? 0 : getNthOccurrence(text, point, realSeparatorOccurrenceIndex); + const newPointPosition = getNthOccurrence(formatted, point, realSeparatorOccurrenceIndex); + const textParts = splitByIndex(text, pointPosition); + const formattedParts = splitByIndex(formatted, newPointPosition); + const isCaretOnFloat = pointPosition !== -1 && caret.start > pointPosition; + + if (isCaretOnFloat) { + const relativeIndex = caret.start - pointPosition - 1; + const digitsBefore = _getDigitCountBeforeIndex(relativeIndex, textParts[1]); + const newPosition = formattedParts[1] ? newPointPosition + 1 + _getDigitPositionByIndex(digitsBefore, formattedParts[1]) + 1 : formatted.length; + + return getCaretInBoundaries(newPosition, formatted, format); + } + const formattedIntPart = _trimNonNumericCharsFromEnd(formattedParts[0]); + const positionFromEnd = textParts[0].length - caret.start; + const digitsFromEnd = _getDigitCountBeforeIndex(positionFromEnd, _reverseText(textParts[0])); + const newPositionFromEnd = _getDigitPositionByIndex(digitsFromEnd, _reverseText(formattedIntPart)); + const newPositionFromBegin = formattedIntPart.length - (newPositionFromEnd + 1); + + return getCaretInBoundaries(newPositionFromBegin, formatted, format); }; function isSeparatorBasedString(text) { - return text.length === 1 && !!text.match(/^[,.][0-9]*$/g); + return text.length === 1 && !!text.match(/^[,.][0-9]*$/g); } -export const isCaretInBoundaries = function(caret, text, format) { - caret = getCaretWithOffset(caret, 0); +export const isCaretInBoundaries = function (caret, text, format) { + caret = getCaretWithOffset(caret, 0); - const boundaries = getCaretInBoundaries(caret, text, format); - return caret.start >= boundaries.start && caret.end <= boundaries.end; + const boundaries = getCaretInBoundaries(caret, text, format); + return caret.start >= boundaries.start && caret.end <= boundaries.end; }; export function getCaretInBoundaries(caret, text, format) { - caret = getCaretWithOffset(caret, 0); + caret = getCaretWithOffset(caret, 0); - const boundaries = getCaretBoundaries(text, format); - const adjustedCaret = { - start: fitIntoRange(caret.start, boundaries.start, boundaries.end), - end: fitIntoRange(caret.end, boundaries.start, boundaries.end) - }; + const boundaries = getCaretBoundaries(text, format); + const adjustedCaret = { + start: fitIntoRange(caret.start, boundaries.start, boundaries.end), + end: fitIntoRange(caret.end, boundaries.start, boundaries.end), + }; - return adjustedCaret; + return adjustedCaret; } -export const getCaretOffset = function(previousText, newText, format) { - const previousBoundaries = getCaretBoundaries(previousText, format); - const newBoundaries = getCaretBoundaries(newText, format); +export const getCaretOffset = function (previousText, newText, format) { + const previousBoundaries = getCaretBoundaries(previousText, format); + const newBoundaries = getCaretBoundaries(newText, format); - return newBoundaries.start - previousBoundaries.start; + return newBoundaries.start - previousBoundaries.start; }; diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts index 510604fecd20..c400b56d2b2a 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts @@ -1,20 +1,27 @@ -import eventsEngine from '../../events/core/events_engine'; -import { name as dxDblClickEvent } from '../../events/double_click'; -import { extend } from '../../core/utils/extend'; -import { isNumeric, isDefined, isFunction, isString } from '../../core/utils/type'; -import devices from '../../core/devices'; -import { fitIntoRange, inRange } from '../../core/utils/math'; - -import number from '../../localization/number'; +import devices from '@js/core/devices'; +import { ensureDefined, escapeRegExp } from '@js/core/utils/common'; +import { extend } from '@js/core/utils/extend'; +import { fitIntoRange, inRange } from '@js/core/utils/math'; import { - getCaretWithOffset, isCaretInBoundaries, getCaretInBoundaries, - getCaretBoundaries, getCaretAfterFormat, getCaretOffset, -} from './number_box.caret'; -import { getFormat as getLDMLFormat } from '../../localization/ldml/number'; -import NumberBoxBase from './number_box.base'; -import { addNamespace, getChar, normalizeKeyName, isCommandKeyPressed } from '../../events/utils/index'; -import { ensureDefined, escapeRegExp } from '../../core/utils/common'; -import { getRealSeparatorIndex, getNthOccurrence, splitByIndex, adjustPercentValue } from './utils'; + isDefined, isFunction, isNumeric, isString, +} from '@js/core/utils/type'; +import eventsEngine from '@js/events/core/events_engine'; +import { name as dxDblClickEvent } from '@js/events/double_click'; +import { + addNamespace, getChar, isCommandKeyPressed, normalizeKeyName, +} from '@js/events/utils/index'; +import { getFormat as getLDMLFormat } from '@js/localization/ldml/number'; +import number from '@js/localization/number'; + +import NumberBoxBase from './m_number_box.base'; +import { + getCaretAfterFormat, getCaretBoundaries, getCaretInBoundaries, + getCaretOffset, + getCaretWithOffset, isCaretInBoundaries, +} from './m_number_box.caret'; +import { + adjustPercentValue, getNthOccurrence, getRealSeparatorIndex, splitByIndex, +} from './m_utils'; const NUMBER_FORMATTER_NAMESPACE = 'dxNumberFormatter'; const MOVE_FORWARD = 1; @@ -28,795 +35,790 @@ const CARET_TIMEOUT_DURATION = 0; const NumberBoxMask = NumberBoxBase.inherit({ - _getDefaultOptions: function() { - return extend(this.callBase(), { + _getDefaultOptions() { + return extend(this.callBase(), { - useMaskBehavior: true, + useMaskBehavior: true, - format: null - }); - }, + format: null, + }); + }, - _isDeleteKey: function(key) { - return key === 'del'; - }, + _isDeleteKey(key) { + return key === 'del'; + }, - _supportedKeys: function() { - if(!this._useMaskBehavior()) { - return this.callBase(); - } + _supportedKeys() { + if (!this._useMaskBehavior()) { + return this.callBase(); + } + + const that = this; + + return extend(this.callBase(), { + minus: that._revertSign.bind(that), + del: that._removeHandler.bind(that), + backspace: that._removeHandler.bind(that), + leftArrow: that._arrowHandler.bind(that, MOVE_BACKWARD), + rightArrow: that._arrowHandler.bind(that, MOVE_FORWARD), + home: that._moveCaretToBoundaryEventHandler.bind(that, MOVE_FORWARD), + enter: that._updateFormattedValue.bind(that), + end: that._moveCaretToBoundaryEventHandler.bind(that, MOVE_BACKWARD), + }); + }, + + _getTextSeparatorIndex(text) { + const decimalSeparator = number.getDecimalSeparator(); + const realSeparatorOccurrenceIndex = getRealSeparatorIndex(this.option('format')).occurrence; + return getNthOccurrence(text, decimalSeparator, realSeparatorOccurrenceIndex); + }, + + _focusInHandler(e) { + if (!this._preventNestedFocusEvent(e)) { + this.clearCaretTimeout(); + this._caretTimeout = setTimeout(() => { + this._caretTimeout = undefined; + const caret = this._caret(); + + if (caret.start === caret.end && this._useMaskBehavior()) { + const text = this._getInputVal(); + const decimalSeparatorIndex = this._getTextSeparatorIndex(text); - const that = this; - - return extend(this.callBase(), { - minus: that._revertSign.bind(that), - del: that._removeHandler.bind(that), - backspace: that._removeHandler.bind(that), - leftArrow: that._arrowHandler.bind(that, MOVE_BACKWARD), - rightArrow: that._arrowHandler.bind(that, MOVE_FORWARD), - home: that._moveCaretToBoundaryEventHandler.bind(that, MOVE_FORWARD), - enter: that._updateFormattedValue.bind(that), - end: that._moveCaretToBoundaryEventHandler.bind(that, MOVE_BACKWARD) - }); - }, - - _getTextSeparatorIndex: function(text) { - const decimalSeparator = number.getDecimalSeparator(); - const realSeparatorOccurrenceIndex = getRealSeparatorIndex(this.option('format')).occurrence; - return getNthOccurrence(text, decimalSeparator, realSeparatorOccurrenceIndex); - }, - - _focusInHandler: function(e) { - if(!this._preventNestedFocusEvent(e)) { - this.clearCaretTimeout(); - this._caretTimeout = setTimeout(function() { - this._caretTimeout = undefined; - const caret = this._caret(); - - if(caret.start === caret.end && this._useMaskBehavior()) { - const text = this._getInputVal(); - const decimalSeparatorIndex = this._getTextSeparatorIndex(text); - - if(decimalSeparatorIndex >= 0) { - this._caret({ start: decimalSeparatorIndex, end: decimalSeparatorIndex }); - } else { - this._moveCaretToBoundaryEventHandler(MOVE_BACKWARD, e); - } - } - }.bind(this), CARET_TIMEOUT_DURATION); + if (decimalSeparatorIndex >= 0) { + this._caret({ start: decimalSeparatorIndex, end: decimalSeparatorIndex }); + } else { + this._moveCaretToBoundaryEventHandler(MOVE_BACKWARD, e); + } } + }, CARET_TIMEOUT_DURATION); + } - this.callBase(e); - }, + this.callBase(e); + }, - _focusOutHandler: function(e) { - const shouldHandleEvent = !this._preventNestedFocusEvent(e); + _focusOutHandler(e) { + const shouldHandleEvent = !this._preventNestedFocusEvent(e); - if(shouldHandleEvent) { - this._focusOutOccurs = true; - if(this._useMaskBehavior()) { - this._updateFormattedValue(); - } - } + if (shouldHandleEvent) { + this._focusOutOccurs = true; + if (this._useMaskBehavior()) { + this._updateFormattedValue(); + } + } - this.callBase(e); + this.callBase(e); - if(shouldHandleEvent) { - this._focusOutOccurs = false; - } - }, + if (shouldHandleEvent) { + this._focusOutOccurs = false; + } + }, - _hasValueBeenChanged(inputValue) { - const format = this._getFormatPattern(); - const value = this.option('value'); - const formatted = this._format(value, format) || ''; + _hasValueBeenChanged(inputValue) { + const format = this._getFormatPattern(); + const value = this.option('value'); + const formatted = this._format(value, format) || ''; - return formatted !== inputValue; - }, + return formatted !== inputValue; + }, - _updateFormattedValue: function() { - const inputValue = this._getInputVal(); + _updateFormattedValue() { + const inputValue = this._getInputVal(); - if(this._hasValueBeenChanged(inputValue)) { - this._updateParsedValue(); + if (this._hasValueBeenChanged(inputValue)) { + this._updateParsedValue(); - this._adjustParsedValue(); - this._setTextByParsedValue(); + this._adjustParsedValue(); + this._setTextByParsedValue(); - if(this._parsedValue !== this.option('value')) { - // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15181565/ - // https://bugreport.apple.com/web/?problemID=38133794 but this bug tracker is private - eventsEngine.trigger(this._input(), 'change'); - } - } - }, + if (this._parsedValue !== this.option('value')) { + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15181565/ + // https://bugreport.apple.com/web/?problemID=38133794 but this bug tracker is private + // @ts-expect-error + eventsEngine.trigger(this._input(), 'change'); + } + } + }, - _arrowHandler: function(step, e) { - if(!this._useMaskBehavior()) { - return; - } + _arrowHandler(step, e) { + if (!this._useMaskBehavior()) { + return; + } - const text = this._getInputVal(); - const format = this._getFormatPattern(); - let nextCaret = getCaretWithOffset(this._caret(), step); + const text = this._getInputVal(); + const format = this._getFormatPattern(); + let nextCaret = getCaretWithOffset(this._caret(), step); - if(!isCaretInBoundaries(nextCaret, text, format)) { - nextCaret = step === MOVE_FORWARD ? nextCaret.end : nextCaret.start; - e.preventDefault(); - this._caret(getCaretInBoundaries(nextCaret, text, format)); - } - }, + if (!isCaretInBoundaries(nextCaret, text, format)) { + nextCaret = step === MOVE_FORWARD ? nextCaret.end : nextCaret.start; + e.preventDefault(); + this._caret(getCaretInBoundaries(nextCaret, text, format)); + } + }, - _moveCaretToBoundary: function(direction) { - const boundaries = getCaretBoundaries(this._getInputVal(), this._getFormatPattern()); - const newCaret = getCaretWithOffset(direction === MOVE_FORWARD ? boundaries.start : boundaries.end, 0); + _moveCaretToBoundary(direction) { + const boundaries = getCaretBoundaries(this._getInputVal(), this._getFormatPattern()); + const newCaret = getCaretWithOffset(direction === MOVE_FORWARD ? boundaries.start : boundaries.end, 0); - this._caret(newCaret); - }, + this._caret(newCaret); + }, - _moveCaretToBoundaryEventHandler: function(direction, e) { - if(!this._useMaskBehavior() || e && e.shiftKey) { - return; - } + _moveCaretToBoundaryEventHandler(direction, e) { + if (!this._useMaskBehavior() || e && e.shiftKey) { + return; + } - this._moveCaretToBoundary(direction); - e && e.preventDefault(); - }, + this._moveCaretToBoundary(direction); + e && e.preventDefault(); + }, - _shouldMoveCaret: function(text, caret) { - const decimalSeparator = number.getDecimalSeparator(); - const isDecimalSeparatorNext = text.charAt(caret.end) === decimalSeparator; - const moveToFloat = (this._lastKey === decimalSeparator || this._lastKey === '.' || this._lastKey === ',') && isDecimalSeparatorNext; + _shouldMoveCaret(text, caret) { + const decimalSeparator = number.getDecimalSeparator(); + const isDecimalSeparatorNext = text.charAt(caret.end) === decimalSeparator; + const moveToFloat = (this._lastKey === decimalSeparator || this._lastKey === '.' || this._lastKey === ',') && isDecimalSeparatorNext; - return moveToFloat; - }, + return moveToFloat; + }, - _getInputVal: function() { - return number.convertDigits(this._input().val(), true); - }, + _getInputVal() { + return number.convertDigits(this._input().val(), true); + }, - _keyboardHandler: function(e) { - this.clearCaretTimeout(); + _keyboardHandler(e) { + this.clearCaretTimeout(); - this._lastKey = number.convertDigits(getChar(e), true); - this._lastKeyName = normalizeKeyName(e); + this._lastKey = number.convertDigits(getChar(e), true); + this._lastKeyName = normalizeKeyName(e); - if(!this._shouldHandleKey(e.originalEvent)) { - return this.callBase(e); - } + if (!this._shouldHandleKey(e.originalEvent)) { + return this.callBase(e); + } - const normalizedText = this._getInputVal(); - const caret = this._caret(); + const normalizedText = this._getInputVal(); + const caret = this._caret(); - let enteredChar; - if(this._lastKeyName === MINUS_KEY) { - enteredChar = ''; - } else { - enteredChar = e.which === NUMPAD_DOT_KEY_CODE ? number.getDecimalSeparator() : this._lastKey; - } - const newValue = this._tryParse(normalizedText, caret, enteredChar); + let enteredChar; + if (this._lastKeyName === MINUS_KEY) { + enteredChar = ''; + } else { + enteredChar = e.which === NUMPAD_DOT_KEY_CODE ? number.getDecimalSeparator() : this._lastKey; + } + const newValue = this._tryParse(normalizedText, caret, enteredChar); - if(this._shouldMoveCaret(normalizedText, caret)) { - this._moveCaret(1); - e.originalEvent.preventDefault(); - } + if (this._shouldMoveCaret(normalizedText, caret)) { + this._moveCaret(1); + e.originalEvent.preventDefault(); + } - if(newValue === undefined) { - if(this._lastKeyName !== MINUS_KEY) { - e.originalEvent.preventDefault(); - } - } else { - this._parsedValue = newValue; - } + if (newValue === undefined) { + if (this._lastKeyName !== MINUS_KEY) { + e.originalEvent.preventDefault(); + } + } else { + this._parsedValue = newValue; + } - return this.callBase(e); - }, + return this.callBase(e); + }, - _keyPressHandler: function(e) { - if(!this._useMaskBehavior()) { - this.callBase(e); - } - }, + _keyPressHandler(e) { + if (!this._useMaskBehavior()) { + this.callBase(e); + } + }, + + _removeHandler(e) { + const caret = this._caret(); + const text = this._getInputVal(); + let { start } = caret; + let { end } = caret; + + this._lastKey = getChar(e); + this._lastKeyName = normalizeKeyName(e); + + const isDeleteKey = this._isDeleteKey(this._lastKeyName); + const isBackspaceKey = !isDeleteKey; + + if (start === end) { + const caretPosition = start; + const canDelete = isBackspaceKey && caretPosition > 0 || isDeleteKey && caretPosition < text.length; + + if (canDelete) { + isDeleteKey && end++; + isBackspaceKey && start--; + } else { + e.preventDefault(); + return; + } + } - _removeHandler: function(e) { - const caret = this._caret(); - const text = this._getInputVal(); - let start = caret.start; - let end = caret.end; - - this._lastKey = getChar(e); - this._lastKeyName = normalizeKeyName(e); - - const isDeleteKey = this._isDeleteKey(this._lastKeyName); - const isBackspaceKey = !isDeleteKey; - - if(start === end) { - const caretPosition = start; - const canDelete = isBackspaceKey && caretPosition > 0 || isDeleteKey && caretPosition < text.length; - - if(canDelete) { - isDeleteKey && end++; - isBackspaceKey && start--; - } else { - e.preventDefault(); - return; - } - } + const char = text.slice(start, end); - const char = text.slice(start, end); - - if(this._isStub(char)) { - this._moveCaret(isDeleteKey ? 1 : -1); - if(this._parsedValue < 0 || 1 / this._parsedValue === -Infinity) { - this._revertSign(e); - this._setTextByParsedValue(); - - const shouldTriggerInputEvent = this.option('valueChangeEvent').split(' ').includes('input'); - if(shouldTriggerInputEvent) { - eventsEngine.trigger(this._input(), 'input'); - } - } - e.preventDefault(); - return; - } + if (this._isStub(char)) { + this._moveCaret(isDeleteKey ? 1 : -1); + if (this._parsedValue < 0 || 1 / this._parsedValue === -Infinity) { + this._revertSign(e); + this._setTextByParsedValue(); - const decimalSeparator = number.getDecimalSeparator(); - if(char === decimalSeparator) { - const decimalSeparatorIndex = text.indexOf(decimalSeparator); - if(this._isNonStubAfter(decimalSeparatorIndex + 1)) { - this._moveCaret(isDeleteKey ? 1 : -1); - e.preventDefault(); - } - return; + const shouldTriggerInputEvent = this.option('valueChangeEvent').split(' ').includes('input'); + if (shouldTriggerInputEvent) { + // @ts-expect-error + eventsEngine.trigger(this._input(), 'input'); } + } + e.preventDefault(); + return; + } + const decimalSeparator = number.getDecimalSeparator(); + if (char === decimalSeparator) { + const decimalSeparatorIndex = text.indexOf(decimalSeparator); + if (this._isNonStubAfter(decimalSeparatorIndex + 1)) { + this._moveCaret(isDeleteKey ? 1 : -1); + e.preventDefault(); + } + return; + } - if(end - start < text.length) { - const editedText = this._replaceSelectedText(text, { start: start, end: end }, ''); - const noDigits = editedText.search(/[0-9]/) < 0; + if (end - start < text.length) { + const editedText = this._replaceSelectedText(text, { start, end }, ''); + const noDigits = editedText.search(/[0-9]/) < 0; - if(noDigits && this._isValueInRange(0)) { - this._parsedValue = this._parsedValue < 0 || 1 / this._parsedValue === -Infinity ? -0 : 0; - return; - } - } + if (noDigits && this._isValueInRange(0)) { + this._parsedValue = this._parsedValue < 0 || 1 / this._parsedValue === -Infinity ? -0 : 0; + return; + } + } - const valueAfterRemoving = this._tryParse(text, { start: start, end: end }, ''); - if(valueAfterRemoving === undefined) { - e.preventDefault(); - } else { - this._parsedValue = valueAfterRemoving; - } - }, + const valueAfterRemoving = this._tryParse(text, { start, end }, ''); + if (valueAfterRemoving === undefined) { + e.preventDefault(); + } else { + this._parsedValue = valueAfterRemoving; + } + }, - _isPercentFormat: function() { - const format = this._getFormatPattern(); - const noEscapedFormat = format.replace(/'[^']+'/g, ''); + _isPercentFormat() { + const format = this._getFormatPattern(); + const noEscapedFormat = format.replace(/'[^']+'/g, ''); - return noEscapedFormat.indexOf('%') !== -1; - }, + return noEscapedFormat.indexOf('%') !== -1; + }, - _parse: function(text, format) { - const formatOption = this.option('format'); - const isCustomParser = isFunction(formatOption.parser); - const parser = isCustomParser ? formatOption.parser : number.parse; - let integerPartStartIndex = 0; + _parse(text, format) { + const formatOption = this.option('format'); + const isCustomParser = isFunction(formatOption.parser); + const parser = isCustomParser ? formatOption.parser : number.parse; + let integerPartStartIndex = 0; - if(!isCustomParser) { - const formatPointIndex = getRealSeparatorIndex(format).index; - const textPointIndex = this._getTextSeparatorIndex(text); + if (!isCustomParser) { + const formatPointIndex = getRealSeparatorIndex(format).index; + const textPointIndex = this._getTextSeparatorIndex(text); - const formatIntegerPartLength = formatPointIndex !== -1 ? formatPointIndex : format.length; - const textIntegerPartLength = textPointIndex !== -1 ? textPointIndex : text.length; + const formatIntegerPartLength = formatPointIndex !== -1 ? formatPointIndex : format.length; + const textIntegerPartLength = textPointIndex !== -1 ? textPointIndex : text.length; - if(textIntegerPartLength > formatIntegerPartLength && format.indexOf('#') === -1) { - integerPartStartIndex = textIntegerPartLength - formatIntegerPartLength; - } - } + if (textIntegerPartLength > formatIntegerPartLength && format.indexOf('#') === -1) { + integerPartStartIndex = textIntegerPartLength - formatIntegerPartLength; + } + } - text = text.substr(integerPartStartIndex); + text = text.substr(integerPartStartIndex); - return parser(text, format); - }, + return parser(text, format); + }, - _format: function(value, format) { - const formatOption = this.option('format'); - const customFormatter = formatOption?.formatter || formatOption; - const formatter = isFunction(customFormatter) ? customFormatter : number.format; + _format(value, format) { + const formatOption = this.option('format'); + const customFormatter = formatOption?.formatter || formatOption; + const formatter = isFunction(customFormatter) ? customFormatter : number.format; - const formattedValue = value === null ? '' : formatter(value, format); + const formattedValue = value === null ? '' : formatter(value, format); - return formattedValue; - }, + return formattedValue; + }, - _getFormatPattern: function() { - if(!this._currentFormat) { - this._updateFormat(); - } + _getFormatPattern() { + if (!this._currentFormat) { + this._updateFormat(); + } - return this._currentFormat; - }, - - _updateFormat: function() { - const format = this.option('format'); - const isCustomParser = isFunction(format?.parser); - const isLDMLPattern = isString(format) && (format.indexOf('0') >= 0 || format.indexOf('#') >= 0); - const isExponentialFormat = format === 'exponential' || format?.type === 'exponential'; - const shouldUseFormatAsIs = isCustomParser || isLDMLPattern || isExponentialFormat; - - this._currentFormat = shouldUseFormatAsIs - ? format - : getLDMLFormat((value) => { - const text = this._format(value, format); - return number.convertDigits(text, true); - }); - }, - - _getFormatForSign: function(text) { - const format = this._getFormatPattern(); - if(isString(format)) { - const signParts = format.split(';'); - const sign = number.getSign(text, format); - - signParts[1] = signParts[1] || '-' + signParts[0]; - return sign < 0 ? signParts[1] : signParts[0]; - } else { - const sign = number.getSign(text); - return sign < 0 ? '-' : ''; - } - }, - - _removeStubs: function(text, excludeComma) { - const format = this._getFormatForSign(text); - const thousandsSeparator = number.getThousandsSeparator(); - const stubs = this._getStubs(format); - let result = text; - - if(stubs.length) { - const prefixStubs = stubs[0]; - const postfixRegex = new RegExp('(' + escapeRegExp(stubs[1] || '') + ')$', 'g'); - const decoratorsRegex = new RegExp('[-' + escapeRegExp((excludeComma ? '' : thousandsSeparator)) + ']', 'g'); - - result = result - .replace(prefixStubs, '') - .replace(postfixRegex, '') - .replace(decoratorsRegex, ''); - } + return this._currentFormat; + }, + + _updateFormat() { + const format = this.option('format'); + const isCustomParser = isFunction(format?.parser); + const isLDMLPattern = isString(format) && (format.includes('0') || format.includes('#')); + const isExponentialFormat = format === 'exponential' || format?.type === 'exponential'; + const shouldUseFormatAsIs = isCustomParser || isLDMLPattern || isExponentialFormat; + + this._currentFormat = shouldUseFormatAsIs + ? format + : getLDMLFormat((value) => { + const text = this._format(value, format); + return number.convertDigits(text, true); + }); + }, + + _getFormatForSign(text) { + const format = this._getFormatPattern(); + if (isString(format)) { + const signParts = format.split(';'); + const sign = number.getSign(text, format); + + signParts[1] = signParts[1] || `-${signParts[0]}`; + return sign < 0 ? signParts[1] : signParts[0]; + } + const sign = number.getSign(text); + return sign < 0 ? '-' : ''; + }, + + _removeStubs(text, excludeComma) { + const format = this._getFormatForSign(text); + const thousandsSeparator = number.getThousandsSeparator(); + const stubs = this._getStubs(format); + let result = text; + + if (stubs.length) { + const prefixStubs = stubs[0]; + const postfixRegex = new RegExp(`(${escapeRegExp(stubs[1] || '')})$`, 'g'); + const decoratorsRegex = new RegExp(`[-${escapeRegExp(excludeComma ? '' : thousandsSeparator)}]`, 'g'); + + result = result + .replace(prefixStubs, '') + .replace(postfixRegex, '') + .replace(decoratorsRegex, ''); + } - return result; - }, - - _getStubs: function(format) { - const regExpResult = /[^']([#0.,]+)/g.exec(format); - const pattern = regExpResult && regExpResult[0].trim(); - - return format - .split(pattern) - .map(function(stub) { - return stub.replace(/'/g, ''); - }); - }, - - _truncateToPrecision: function(value, maxPrecision) { - if(isDefined(value)) { - const strValue = value.toString(); - const decimalSeparatorIndex = strValue.indexOf('.'); - - if(strValue && decimalSeparatorIndex > -1) { - const parsedValue = parseFloat(strValue.substr(0, decimalSeparatorIndex + maxPrecision + 1)); - return isNaN(parsedValue) ? value : parsedValue; - } - } - return value; - }, + return result; + }, - _tryParse: function(text, selection, char) { - const isTextSelected = selection.start !== selection.end; - const isWholeTextSelected = isTextSelected && selection.start === 0 && selection.end === text.length; - const decimalSeparator = number.getDecimalSeparator(); + _getStubs(format) { + const regExpResult = /[^']([#0.,]+)/g.exec(format); + const pattern = regExpResult && regExpResult[0].trim(); - if(isWholeTextSelected && char === decimalSeparator) { - return 0; - } + return format + .split(pattern) + .map((stub) => stub.replace(/'/g, '')); + }, - const editedText = this._replaceSelectedText(text, selection, char); - const format = this._getFormatPattern(); + _truncateToPrecision(value, maxPrecision) { + if (isDefined(value)) { + const strValue = value.toString(); + const decimalSeparatorIndex = strValue.indexOf('.'); + + if (strValue && decimalSeparatorIndex > -1) { + const parsedValue = parseFloat(strValue.substr(0, decimalSeparatorIndex + maxPrecision + 1)); + return isNaN(parsedValue) ? value : parsedValue; + } + } + return value; + }, + + _tryParse(text, selection, char) { + const isTextSelected = selection.start !== selection.end; + const isWholeTextSelected = isTextSelected && selection.start === 0 && selection.end === text.length; + const decimalSeparator = number.getDecimalSeparator(); + + if (isWholeTextSelected && char === decimalSeparator) { + return 0; + } + + const editedText = this._replaceSelectedText(text, selection, char); + const format = this._getFormatPattern(); - let parsedValue = this._getParsedValue(editedText, format); - const maxPrecision = !format.parser && this._getPrecisionLimits(editedText).max; - const isValueChanged = parsedValue !== this._parsedValue; + let parsedValue = this._getParsedValue(editedText, format); + const maxPrecision = !format.parser && this._getPrecisionLimits(editedText).max; + const isValueChanged = parsedValue !== this._parsedValue; - const isDecimalPointRestricted = char === decimalSeparator && maxPrecision === 0; - const isUselessCharRestricted = !isTextSelected + const isDecimalPointRestricted = char === decimalSeparator && maxPrecision === 0; + const isUselessCharRestricted = !isTextSelected && !isValueChanged && char !== MINUS && this._isStub(char); - if(isDecimalPointRestricted || isUselessCharRestricted) { - return undefined; - } - - if(this._removeStubs(editedText) === '') { - parsedValue = Math.abs(this._parsedValue * 0); - } + if (isDecimalPointRestricted || isUselessCharRestricted) { + return undefined; + } - if(isNaN(parsedValue)) { - return undefined; - } + if (this._removeStubs(editedText) === '') { + parsedValue = Math.abs(this._parsedValue * 0); + } - const value = parsedValue === null ? this._parsedValue : parsedValue; - parsedValue = maxPrecision ? this._truncateToPrecision(value, maxPrecision) : parsedValue; + if (isNaN(parsedValue)) { + return undefined; + } - return !format.parser && this._isPercentFormat() ? adjustPercentValue(parsedValue, maxPrecision) : parsedValue; - }, + const value = parsedValue === null ? this._parsedValue : parsedValue; + parsedValue = maxPrecision ? this._truncateToPrecision(value, maxPrecision) : parsedValue; - _getParsedValue: function(text, format) { - const sign = number.getSign(text, format?.formatter || format); - const textWithoutStubs = this._removeStubs(text, true); - const parsedValue = this._parse(textWithoutStubs, format); - const parsedValueSign = parsedValue < 0 ? -1 : 1; - const parsedValueWithSign = isNumeric(parsedValue) && sign !== parsedValueSign ? sign * parsedValue : parsedValue; + return !format.parser && this._isPercentFormat() ? adjustPercentValue(parsedValue, maxPrecision) : parsedValue; + }, - return parsedValueWithSign; - }, + _getParsedValue(text, format) { + const sign = number.getSign(text, format?.formatter || format); + const textWithoutStubs = this._removeStubs(text, true); + const parsedValue = this._parse(textWithoutStubs, format); + const parsedValueSign = parsedValue < 0 ? -1 : 1; + const parsedValueWithSign = isNumeric(parsedValue) && sign !== parsedValueSign ? sign * parsedValue : parsedValue; - _isValueIncomplete: function(text) { - if(!this._useMaskBehavior()) { - return this.callBase(text); - } + return parsedValueWithSign; + }, - const caret = this._caret(); - const point = number.getDecimalSeparator(); - const pointIndex = this._getTextSeparatorIndex(text); - const isCaretOnFloat = pointIndex >= 0 && pointIndex < caret.start; - const textParts = this._removeStubs(text, true).split(point); + _isValueIncomplete(text) { + if (!this._useMaskBehavior()) { + return this.callBase(text); + } - if(!isCaretOnFloat || textParts.length !== 2) { - return false; - } + const caret = this._caret(); + const point = number.getDecimalSeparator(); + const pointIndex = this._getTextSeparatorIndex(text); + const isCaretOnFloat = pointIndex >= 0 && pointIndex < caret.start; + const textParts = this._removeStubs(text, true).split(point); - const floatLength = textParts[1].length; - const format = this._getFormatPattern(); - const isCustomParser = !!format.parser; - const precision = !isCustomParser && this._getPrecisionLimits(this._getFormatPattern(), text); - const isPrecisionInRange = isCustomParser ? true : inRange(floatLength, precision.min, precision.max); - const endsWithZero = textParts[1].charAt(floatLength - 1) === '0'; + if (!isCaretOnFloat || textParts.length !== 2) { + return false; + } - return isPrecisionInRange && (endsWithZero || !floatLength); - }, + const floatLength = textParts[1].length; + const format = this._getFormatPattern(); + const isCustomParser = !!format.parser; + const precision = !isCustomParser && this._getPrecisionLimits(this._getFormatPattern(), text); + const isPrecisionInRange = isCustomParser ? true : inRange(floatLength, precision.min, precision.max); + const endsWithZero = textParts[1].charAt(floatLength - 1) === '0'; - _isValueInRange: function(value) { - const min = ensureDefined(this.option('min'), -Infinity); - const max = ensureDefined(this.option('max'), Infinity); + return isPrecisionInRange && (endsWithZero || !floatLength); + }, - return inRange(value, min, max); - }, + _isValueInRange(value) { + const min = ensureDefined(this.option('min'), -Infinity); + const max = ensureDefined(this.option('max'), Infinity); - _setInputText: function(text) { - const normalizedText = number.convertDigits(text, true); - const newCaret = getCaretAfterFormat(this._getInputVal(), normalizedText, this._caret(), this._getFormatPattern()); + return inRange(value, min, max); + }, - this._input().val(text); - this._toggleEmptinessEventHandler(); - this._formattedValue = text; + _setInputText(text) { + const normalizedText = number.convertDigits(text, true); + const newCaret = getCaretAfterFormat(this._getInputVal(), normalizedText, this._caret(), this._getFormatPattern()); - if(!this._focusOutOccurs) { - this._caret(newCaret); - } - }, + this._input().val(text); + this._toggleEmptinessEventHandler(); + this._formattedValue = text; - _useMaskBehavior: function() { - return !!this.option('format') && this.option('useMaskBehavior'); - }, + if (!this._focusOutOccurs) { + this._caret(newCaret); + } + }, - _renderInputType: function() { - const isNumberType = this.option('mode') === 'number'; - const isDesktop = devices.real().deviceType === 'desktop'; + _useMaskBehavior() { + return !!this.option('format') && this.option('useMaskBehavior'); + }, - if(this._useMaskBehavior() && isNumberType) { - this._setInputType(isDesktop || this._isSupportInputMode() ? 'text' : 'tel'); - } else { - this.callBase(); - } - }, + _renderInputType() { + const isNumberType = this.option('mode') === 'number'; + const isDesktop = devices.real().deviceType === 'desktop'; - _isChar: function(str) { - return isString(str) && str.length === 1; - }, + if (this._useMaskBehavior() && isNumberType) { + this._setInputType(isDesktop || this._isSupportInputMode() ? 'text' : 'tel'); + } else { + this.callBase(); + } + }, - _moveCaret: function(offset) { - if(!offset) { - return; - } + _isChar(str) { + return isString(str) && str.length === 1; + }, - const newCaret = getCaretWithOffset(this._caret(), offset); - const adjustedCaret = getCaretInBoundaries(newCaret, this._getInputVal(), this._getFormatPattern()); + _moveCaret(offset) { + if (!offset) { + return; + } - this._caret(adjustedCaret); - }, + const newCaret = getCaretWithOffset(this._caret(), offset); + const adjustedCaret = getCaretInBoundaries(newCaret, this._getInputVal(), this._getFormatPattern()); - _shouldHandleKey: function(e) { - const keyName = normalizeKeyName(e); - const isSpecialChar = isCommandKeyPressed(e) || e.altKey || e.shiftKey || !this._isChar(keyName); - const isMinusKey = keyName === MINUS_KEY; - const useMaskBehavior = this._useMaskBehavior(); + this._caret(adjustedCaret); + }, - return useMaskBehavior && !isSpecialChar && !isMinusKey; - }, + _shouldHandleKey(e) { + const keyName = normalizeKeyName(e); + const isSpecialChar = isCommandKeyPressed(e) || e.altKey || e.shiftKey || !this._isChar(keyName); + const isMinusKey = keyName === MINUS_KEY; + const useMaskBehavior = this._useMaskBehavior(); - _renderInput: function() { - this.callBase(); - this._renderFormatter(); - }, + return useMaskBehavior && !isSpecialChar && !isMinusKey; + }, - _renderFormatter: function() { - this._clearCache(); - this._detachFormatterEvents(); + _renderInput() { + this.callBase(); + this._renderFormatter(); + }, - if(this._useMaskBehavior()) { - this._attachFormatterEvents(); - } - }, + _renderFormatter() { + this._clearCache(); + this._detachFormatterEvents(); - _detachFormatterEvents: function() { - eventsEngine.off(this._input(), '.' + NUMBER_FORMATTER_NAMESPACE); - }, + if (this._useMaskBehavior()) { + this._attachFormatterEvents(); + } + }, - _isInputFromPaste: function(e) { - const inputType = e.originalEvent && e.originalEvent.inputType; + _detachFormatterEvents() { + eventsEngine.off(this._input(), `.${NUMBER_FORMATTER_NAMESPACE}`); + }, - if(isDefined(inputType)) { - return inputType === 'insertFromPaste'; - } else { - return this._isValuePasted; - } - }, - - _attachFormatterEvents: function() { - const $input = this._input(); - - eventsEngine.on($input, addNamespace(INPUT_EVENT, NUMBER_FORMATTER_NAMESPACE), function(e) { - this._formatValue(e); - this._isValuePasted = false; - }.bind(this)); - - eventsEngine.on($input, addNamespace('dxclick', NUMBER_FORMATTER_NAMESPACE), function() { - if(!this._caretTimeout) { - this._caretTimeout = setTimeout(function() { - this._caretTimeout = undefined; - this._caret(getCaretInBoundaries(this._caret(), this._getInputVal(), this._getFormatPattern())); - }.bind(this), CARET_TIMEOUT_DURATION); - } - }.bind(this)); - - eventsEngine.on($input, dxDblClickEvent, function() { - this.clearCaretTimeout(); - }.bind(this)); - }, - - clearCaretTimeout: function() { - clearTimeout(this._caretTimeout); - this._caretTimeout = undefined; - }, + _isInputFromPaste(e) { + const inputType = e.originalEvent && e.originalEvent.inputType; - _forceRefreshInputValue: function() { - if(!this._useMaskBehavior()) { - return this.callBase(); - } - }, + if (isDefined(inputType)) { + return inputType === 'insertFromPaste'; + } + return this._isValuePasted; + }, + + _attachFormatterEvents() { + const $input = this._input(); + + eventsEngine.on($input, addNamespace(INPUT_EVENT, NUMBER_FORMATTER_NAMESPACE), (e) => { + this._formatValue(e); + this._isValuePasted = false; + }); + + eventsEngine.on($input, addNamespace('dxclick', NUMBER_FORMATTER_NAMESPACE), () => { + if (!this._caretTimeout) { + this._caretTimeout = setTimeout(() => { + this._caretTimeout = undefined; + this._caret(getCaretInBoundaries(this._caret(), this._getInputVal(), this._getFormatPattern())); + }, CARET_TIMEOUT_DURATION); + } + }); + + eventsEngine.on($input, dxDblClickEvent, () => { + this.clearCaretTimeout(); + }); + }, + + clearCaretTimeout() { + clearTimeout(this._caretTimeout); + this._caretTimeout = undefined; + }, + + _forceRefreshInputValue() { + if (!this._useMaskBehavior()) { + return this.callBase(); + } + }, - _isNonStubAfter: function(index) { - const text = this._getInputVal().slice(index); - return text && !this._isStub(text, true); - }, + _isNonStubAfter(index) { + const text = this._getInputVal().slice(index); + return text && !this._isStub(text, true); + }, - _isStub: function(str, isString) { - const escapedDecimalSeparator = escapeRegExp(number.getDecimalSeparator()); - const regExpString = '^[^0-9' + escapedDecimalSeparator + ']+$'; - const stubRegExp = new RegExp(regExpString, 'g'); + _isStub(str, isString) { + const escapedDecimalSeparator = escapeRegExp(number.getDecimalSeparator()); + const regExpString = `^[^0-9${escapedDecimalSeparator}]+$`; + const stubRegExp = new RegExp(regExpString, 'g'); - return stubRegExp.test(str) && (isString || this._isChar(str)); - }, + return stubRegExp.test(str) && (isString || this._isChar(str)); + }, - _parseValue: function(text) { - if(!this._useMaskBehavior()) { - return this.callBase(text); - } + _parseValue(text) { + if (!this._useMaskBehavior()) { + return this.callBase(text); + } - return this._parsedValue; - }, + return this._parsedValue; + }, - _getPrecisionLimits: function(text) { - const currentFormat = this._getFormatForSign(text); - const realSeparatorIndex = getRealSeparatorIndex(currentFormat).index; - const floatPart = (splitByIndex(currentFormat, realSeparatorIndex)[1] || '').replace(/[^#0]/g, ''); - const minPrecision = floatPart.replace(/^(0*)#*/, '$1').length; - const maxPrecision = floatPart.length; + _getPrecisionLimits(text) { + const currentFormat = this._getFormatForSign(text); + const realSeparatorIndex = getRealSeparatorIndex(currentFormat).index; + const floatPart = (splitByIndex(currentFormat, realSeparatorIndex)[1] || '').replace(/[^#0]/g, ''); + const minPrecision = floatPart.replace(/^(0*)#*/, '$1').length; + const maxPrecision = floatPart.length; - return { min: minPrecision, max: maxPrecision }; - }, + return { min: minPrecision, max: maxPrecision }; + }, - _revertSign: function(e) { - if(!this._useMaskBehavior()) { - return; - } + _revertSign(e) { + if (!this._useMaskBehavior()) { + return; + } - const caret = this._caret(); - if(caret.start !== caret.end) { - if(normalizeKeyName(e) === MINUS_KEY) { - this._applyRevertedSign(e, caret, true); - return; - } else { - this._caret(getCaretInBoundaries(0, this._getInputVal(), this._getFormatPattern())); - } + const caret = this._caret(); + if (caret.start !== caret.end) { + if (normalizeKeyName(e) === MINUS_KEY) { + this._applyRevertedSign(e, caret, true); + return; + } + this._caret(getCaretInBoundaries(0, this._getInputVal(), this._getFormatPattern())); + } - } + this._applyRevertedSign(e, caret); + }, - this._applyRevertedSign(e, caret); - }, + _applyRevertedSign(e, caret, preserveSelectedText) { + const newValue = -1 * ensureDefined(this._parsedValue, null); - _applyRevertedSign: function(e, caret, preserveSelectedText) { - const newValue = -1 * ensureDefined(this._parsedValue, null); + if (this._isValueInRange(newValue) || newValue === 0) { + this._parsedValue = newValue; - if(this._isValueInRange(newValue) || newValue === 0) { - this._parsedValue = newValue; + if (preserveSelectedText) { + const format = this._getFormatPattern(); + const previousText = this._getInputVal(); - if(preserveSelectedText) { - const format = this._getFormatPattern(); - const previousText = this._getInputVal(); + this._setTextByParsedValue(); + e.preventDefault(); - this._setTextByParsedValue(); - e.preventDefault(); + const currentText = this._getInputVal(); + const offset = getCaretOffset(previousText, currentText, format); - const currentText = this._getInputVal(); - const offset = getCaretOffset(previousText, currentText, format); + caret = getCaretWithOffset(caret, offset); - caret = getCaretWithOffset(caret, offset); + const caretInBoundaries = getCaretInBoundaries(caret, currentText, format); - const caretInBoundaries = getCaretInBoundaries(caret, currentText, format); + this._caret(caretInBoundaries); + } + } + }, - this._caret(caretInBoundaries); - } - } - }, + _removeMinusFromText(text, caret) { + const isMinusPressed = this._lastKeyName === MINUS_KEY && text.charAt(caret.start - 1) === MINUS; - _removeMinusFromText: function(text, caret) { - const isMinusPressed = this._lastKeyName === MINUS_KEY && text.charAt(caret.start - 1) === MINUS; + return isMinusPressed ? this._replaceSelectedText(text, { + start: caret.start - 1, + end: caret.start, + }, '') : text; + }, - return isMinusPressed ? this._replaceSelectedText(text, { - start: caret.start - 1, - end: caret.start - }, '') : text; - }, + _setTextByParsedValue() { + const format = this._getFormatPattern(); + const parsed = this._parseValue(); + const formatted = this._format(parsed, format) || ''; - _setTextByParsedValue: function() { - const format = this._getFormatPattern(); - const parsed = this._parseValue(); - const formatted = this._format(parsed, format) || ''; + this._setInputText(formatted); + }, - this._setInputText(formatted); - }, + _formatValue(e) { + let normalizedText = this._getInputVal(); + const caret = this._caret(); + const textWithoutMinus = this._removeMinusFromText(normalizedText, caret); + const wasMinusRemoved = textWithoutMinus !== normalizedText; - _formatValue: function(e) { - let normalizedText = this._getInputVal(); - const caret = this._caret(); - const textWithoutMinus = this._removeMinusFromText(normalizedText, caret); - const wasMinusRemoved = textWithoutMinus !== normalizedText; + normalizedText = textWithoutMinus; - normalizedText = textWithoutMinus; + if (!this._isInputFromPaste(e) && this._isValueIncomplete(textWithoutMinus)) { + this._formattedValue = normalizedText; + if (wasMinusRemoved) { + this._setTextByParsedValue(); + } + return; + } - if(!this._isInputFromPaste(e) && this._isValueIncomplete(textWithoutMinus)) { - this._formattedValue = normalizedText; - if(wasMinusRemoved) { - this._setTextByParsedValue(); - } - return; - } + const textWasChanged = number.convertDigits(this._formattedValue, true) !== normalizedText; + if (textWasChanged) { + const value = this._tryParse(normalizedText, caret, ''); + if (isDefined(value)) { + this._parsedValue = value; + } + } - const textWasChanged = number.convertDigits(this._formattedValue, true) !== normalizedText; - if(textWasChanged) { - const value = this._tryParse(normalizedText, caret, ''); - if(isDefined(value)) { - this._parsedValue = value; - } - } + this._setTextByParsedValue(); + }, - this._setTextByParsedValue(); - }, + _renderDisplayText() { + if (this._useMaskBehavior()) { + this._toggleEmptinessEventHandler(); + } else { + this.callBase.apply(this, arguments); + } + }, - _renderDisplayText: function() { - if(this._useMaskBehavior()) { - this._toggleEmptinessEventHandler(); - } else { - this.callBase.apply(this, arguments); - } - }, + _renderValue() { + if (this._useMaskBehavior()) { + this._parsedValue = this.option('value'); + this._setTextByParsedValue(); + } - _renderValue: function() { - if(this._useMaskBehavior()) { - this._parsedValue = this.option('value'); - this._setTextByParsedValue(); - } + return this.callBase(); + }, - return this.callBase(); - }, + _updateParsedValue() { + const inputValue = this._getInputVal(); + this._parsedValue = this._tryParse(inputValue, this._caret()); + }, - _updateParsedValue: function() { - const inputValue = this._getInputVal(); - this._parsedValue = this._tryParse(inputValue, this._caret()); - }, + _adjustParsedValue() { + if (!this._useMaskBehavior()) { + return; + } - _adjustParsedValue: function() { - if(!this._useMaskBehavior()) { - return; - } + const clearedText = this._removeStubs(this._getInputVal()); + const parsedValue = clearedText ? this._parseValue() : null; - const clearedText = this._removeStubs(this._getInputVal()); - const parsedValue = clearedText ? this._parseValue() : null; + if (!isNumeric(parsedValue)) { + this._parsedValue = parsedValue; + return; + } - if(!isNumeric(parsedValue)) { - this._parsedValue = parsedValue; - return; - } + this._parsedValue = fitIntoRange(parsedValue, this.option('min'), this.option('max')); + }, - this._parsedValue = fitIntoRange(parsedValue, this.option('min'), this.option('max')); - }, + _valueChangeEventHandler(e) { + if (!this._useMaskBehavior()) { + return this.callBase(e); + } - _valueChangeEventHandler: function(e) { - if(!this._useMaskBehavior()) { - return this.callBase(e); - } + const caret = this._caret(); - const caret = this._caret(); + this._saveValueChangeEvent(e); + this._lastKey = null; + this._lastKeyName = null; - this._saveValueChangeEvent(e); - this._lastKey = null; - this._lastKeyName = null; + this._updateParsedValue(); + this._adjustParsedValue(); + this.option('value', this._parsedValue); - this._updateParsedValue(); + if (caret) { + this._caret(caret); + } + }, + + _optionChanged(args) { + switch (args.name) { + case 'format': + case 'useMaskBehavior': + this._renderInputType(); + this._updateFormat(); + this._renderFormatter(); + this._renderValue(); + this._refreshValueChangeEvent(); + this._refreshEvents(); + break; + case 'min': + case 'max': this._adjustParsedValue(); - this.option('value', this._parsedValue); - - if(caret) { - this._caret(caret); - } - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'format': - case 'useMaskBehavior': - this._renderInputType(); - this._updateFormat(); - this._renderFormatter(); - this._renderValue(); - this._refreshValueChangeEvent(); - this._refreshEvents(); - break; - case 'min': - case 'max': - this._adjustParsedValue(); - this.callBase(args); - break; - default: - this.callBase(args); - } - }, - - _clearCache: function() { - delete this._formattedValue; - delete this._lastKey; - delete this._lastKeyName; - delete this._parsedValue; - delete this._focusOutOccurs; - clearTimeout(this._caretTimeout); - delete this._caretTimeout; - }, - - _clean: function() { - this._clearCache(); - this.callBase(); + this.callBase(args); + break; + default: + this.callBase(args); } + }, + + _clearCache() { + delete this._formattedValue; + delete this._lastKey; + delete this._lastKeyName; + delete this._parsedValue; + delete this._focusOutOccurs; + clearTimeout(this._caretTimeout); + delete this._caretTimeout; + }, + + _clean() { + this._clearCache(); + this.callBase(); + }, }); export default NumberBoxMask; diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts index f65b373ad776..e793737c9572 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts @@ -1,13 +1,13 @@ -import $ from '../../core/renderer'; -import domAdapter from '../../core/dom_adapter'; -import eventsEngine from '../../events/core/events_engine'; -import Widget from '../widget/ui.widget'; -import { extend } from '../../core/utils/extend'; -import { addNamespace } from '../../events/utils/index'; -import pointerEvents from '../../events/pointer'; -import { lock } from '../../events/core/emitter.feedback'; -import holdEvent from '../../events/hold'; -import { Deferred } from '../../core/utils/deferred'; +import domAdapter from '@js/core/dom_adapter'; +import $ from '@js/core/renderer'; +import { Deferred } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { lock } from '@js/events/core/emitter.feedback'; +import eventsEngine from '@js/events/core/events_engine'; +import holdEvent from '@js/events/hold'; +import pointerEvents from '@js/events/pointer'; +import { addNamespace } from '@js/events/utils/index'; +import Widget from '@js/ui/widget/ui.widget'; const SPIN_CLASS = 'dx-numberbox-spin'; const SPIN_BUTTON_CLASS = 'dx-numberbox-spin-button'; @@ -17,91 +17,91 @@ const SPIN_HOLD_DELAY = 100; const NUMBER_BOX = 'dxNumberBox'; const POINTERUP_EVENT_NAME = addNamespace(pointerEvents.up, NUMBER_BOX); const POINTERCANCEL_EVENT_NAME = addNamespace(pointerEvents.cancel, NUMBER_BOX); - +// @ts-expect-error const SpinButton = Widget.inherit({ - _getDefaultOptions: function() { - return extend(this.callBase(), { - direction: 'up', - onChange: null, - activeStateEnabled: true, - hoverStateEnabled: true - }); - }, - - _initMarkup: function() { - this.callBase(); - - const direction = SPIN_CLASS + '-' + this.option('direction'); - - this.$element() - .addClass(SPIN_BUTTON_CLASS) - .addClass(direction); - - this._spinIcon = $('
').addClass(direction + '-icon').appendTo(this.$element()); - }, - - _render: function() { - this.callBase(); - - const eventName = addNamespace(pointerEvents.down, this.NAME); - const $element = this.$element(); - - eventsEngine.off($element, eventName); - eventsEngine.on($element, eventName, this._spinDownHandler.bind(this)); - - this._spinChangeHandler = this._createActionByOption('onChange'); - }, - - _spinDownHandler: function(e) { - e.preventDefault(); - - this._clearTimer(); - - eventsEngine.on(this.$element(), holdEvent.name, (function() { - this._feedBackDeferred = new Deferred(); - lock(this._feedBackDeferred); - this._spinChangeHandler({ event: e }); - this._holdTimer = setInterval(this._spinChangeHandler, SPIN_HOLD_DELAY, { event: e }); - }).bind(this)); - - const document = domAdapter.getDocument(); - eventsEngine.on(document, POINTERUP_EVENT_NAME, this._clearTimer.bind(this)); - eventsEngine.on(document, POINTERCANCEL_EVENT_NAME, this._clearTimer.bind(this)); - - this._spinChangeHandler({ event: e }); - }, - - _dispose: function() { - this._clearTimer(); - this.callBase(); - }, - - _clearTimer: function() { - eventsEngine.off(this.$element(), holdEvent.name); - - const document = domAdapter.getDocument(); - eventsEngine.off(document, POINTERUP_EVENT_NAME); - eventsEngine.off(document, POINTERCANCEL_EVENT_NAME); - - if(this._feedBackDeferred) { - this._feedBackDeferred.resolve(); - } - if(this._holdTimer) { - clearInterval(this._holdTimer); - } - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'onChange': - case 'direction': - this._invalidate(); - break; - default: - this.callBase(args); - } + _getDefaultOptions() { + return extend(this.callBase(), { + direction: 'up', + onChange: null, + activeStateEnabled: true, + hoverStateEnabled: true, + }); + }, + + _initMarkup() { + this.callBase(); + + const direction = `${SPIN_CLASS}-${this.option('direction')}`; + + this.$element() + .addClass(SPIN_BUTTON_CLASS) + .addClass(direction); + + this._spinIcon = $('
').addClass(`${direction}-icon`).appendTo(this.$element()); + }, + + _render() { + this.callBase(); + + const eventName = addNamespace(pointerEvents.down, this.NAME); + const $element = this.$element(); + + eventsEngine.off($element, eventName); + eventsEngine.on($element, eventName, this._spinDownHandler.bind(this)); + + this._spinChangeHandler = this._createActionByOption('onChange'); + }, + + _spinDownHandler(e) { + e.preventDefault(); + + this._clearTimer(); + + eventsEngine.on(this.$element(), holdEvent.name, () => { + this._feedBackDeferred = Deferred(); + lock(this._feedBackDeferred); + this._spinChangeHandler({ event: e }); + this._holdTimer = setInterval(this._spinChangeHandler, SPIN_HOLD_DELAY, { event: e }); + }); + + const document = domAdapter.getDocument(); + eventsEngine.on(document, POINTERUP_EVENT_NAME, this._clearTimer.bind(this)); + eventsEngine.on(document, POINTERCANCEL_EVENT_NAME, this._clearTimer.bind(this)); + + this._spinChangeHandler({ event: e }); + }, + + _dispose() { + this._clearTimer(); + this.callBase(); + }, + + _clearTimer() { + eventsEngine.off(this.$element(), holdEvent.name); + + const document = domAdapter.getDocument(); + eventsEngine.off(document, POINTERUP_EVENT_NAME); + eventsEngine.off(document, POINTERCANCEL_EVENT_NAME); + + if (this._feedBackDeferred) { + this._feedBackDeferred.resolve(); + } + if (this._holdTimer) { + clearInterval(this._holdTimer); + } + }, + + _optionChanged(args) { + switch (args.name) { + case 'onChange': + case 'direction': + this._invalidate(); + break; + default: + this.callBase(args); } + }, }); export default SpinButton; diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts index 9fa7c1b51736..edd241e304b1 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts @@ -1,104 +1,110 @@ -import $ from '../../core/renderer'; -import eventsEngine from '../../events/core/events_engine'; -import TextEditorButton from '../../__internal/ui/text_box/texteditor_button_collection/m_button'; -import SpinButton from './number_box.spin'; -import { addNamespace } from '../../events/utils/index'; -import pointer from '../../events/pointer'; -import { extend } from '../../core/utils/extend'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import eventsEngine from '@js/events/core/events_engine'; +import pointer from '@js/events/pointer'; +import { addNamespace } from '@js/events/utils/index'; +import TextEditorButton from '@ts/ui/text_box/texteditor_button_collection/m_button'; + +import SpinButton from './m_number_box.spin'; const SPIN_CLASS = 'dx-numberbox-spin'; const SPIN_CONTAINER_CLASS = 'dx-numberbox-spin-container'; const SPIN_TOUCH_FRIENDLY_CLASS = 'dx-numberbox-spin-touch-friendly'; export default class SpinButtons extends TextEditorButton { - _attachEvents(instance, $spinContainer) { - const { editor } = this; - const eventName = addNamespace(pointer.down, editor.NAME); - const $spinContainerChildren = $spinContainer.children(); - const pointerDownAction = editor._createAction( - (e) => editor._spinButtonsPointerDownHandler(e) - ); - - eventsEngine.off($spinContainer, eventName); - eventsEngine.on($spinContainer, eventName, - (e) => pointerDownAction({ event: e }) - ); - - SpinButton.getInstance($spinContainerChildren.eq(0)).option('onChange', - (e) => editor._spinUpChangeHandler(e) - ); - - SpinButton.getInstance($spinContainerChildren.eq(1)).option('onChange', - (e) => editor._spinDownChangeHandler(e) - ); - } - - _create() { - const { editor } = this; - const $spinContainer = $('
').addClass(SPIN_CONTAINER_CLASS); - const $spinUp = $('
').appendTo($spinContainer); - const $spinDown = $('
').appendTo($spinContainer); - const options = this._getOptions(); - - this._addToContainer($spinContainer); - - editor._createComponent($spinUp, SpinButton, extend({ direction: 'up' }, options)); - editor._createComponent($spinDown, SpinButton, extend({ direction: 'down' }, options)); - - this._legacyRender(editor.$element(), this._isTouchFriendly(), options.visible); - - return { - instance: $spinContainer, - $element: $spinContainer - }; - } - - _getOptions() { - const { editor } = this; - const visible = this._isVisible(); - const disabled = editor.option('disabled'); - - return { - visible, - disabled - }; - } - - _isVisible() { - const { editor } = this; - - return super._isVisible() && editor.option('showSpinButtons'); - } - - _isTouchFriendly() { - const { editor } = this; - - return editor.option('showSpinButtons') && editor.option('useLargeSpinButtons'); - } - - // TODO: get rid of it - _legacyRender($editor, isTouchFriendly, isVisible) { - $editor.toggleClass(SPIN_TOUCH_FRIENDLY_CLASS, isTouchFriendly); - $editor.toggleClass(SPIN_CLASS, isVisible); - } - - update() { - const shouldUpdate = super.update(); - - if(shouldUpdate) { - const { editor, instance } = this; - const $editor = editor.$element(); - const isVisible = this._isVisible(); - const isTouchFriendly = this._isTouchFriendly(); - const $spinButtons = instance.children(); - const spinUp = SpinButton.getInstance($spinButtons.eq(0)); - const spinDown = SpinButton.getInstance($spinButtons.eq(1)); - const options = this._getOptions(); - - spinUp.option(options); - spinDown.option(options); - - this._legacyRender($editor, isTouchFriendly, isVisible); - } + _attachEvents(instance, $spinContainer) { + const { editor } = this; + const eventName = addNamespace(pointer.down, editor.NAME); + const $spinContainerChildren = $spinContainer.children(); + const pointerDownAction = editor._createAction( + (e) => editor._spinButtonsPointerDownHandler(e), + ); + + eventsEngine.off($spinContainer, eventName); + eventsEngine.on( + $spinContainer, + eventName, + (e) => pointerDownAction({ event: e }), + ); + + SpinButton.getInstance($spinContainerChildren.eq(0)).option( + 'onChange', + (e) => editor._spinUpChangeHandler(e), + ); + + SpinButton.getInstance($spinContainerChildren.eq(1)).option( + 'onChange', + (e) => editor._spinDownChangeHandler(e), + ); + } + + _create() { + const { editor } = this; + const $spinContainer = $('
').addClass(SPIN_CONTAINER_CLASS); + const $spinUp = $('
').appendTo($spinContainer); + const $spinDown = $('
').appendTo($spinContainer); + const options = this._getOptions(); + + this._addToContainer($spinContainer); + + editor._createComponent($spinUp, SpinButton, extend({ direction: 'up' }, options)); + editor._createComponent($spinDown, SpinButton, extend({ direction: 'down' }, options)); + + this._legacyRender(editor.$element(), this._isTouchFriendly(), options.visible); + + return { + instance: $spinContainer, + $element: $spinContainer, + }; + } + + _getOptions() { + const { editor } = this; + const visible = this._isVisible(); + const disabled = editor.option('disabled'); + + return { + visible, + disabled, + }; + } + + _isVisible() { + const { editor } = this; + + return super._isVisible() && editor.option('showSpinButtons'); + } + + _isTouchFriendly() { + const { editor } = this; + + return editor.option('showSpinButtons') && editor.option('useLargeSpinButtons'); + } + + // TODO: get rid of it + _legacyRender($editor, isTouchFriendly, isVisible) { + $editor.toggleClass(SPIN_TOUCH_FRIENDLY_CLASS, isTouchFriendly); + $editor.toggleClass(SPIN_CLASS, isVisible); + } + + // @ts-expect-error + update() { + const shouldUpdate = super.update(); + + if (shouldUpdate) { + const { editor, instance } = this; + const $editor = editor.$element(); + const isVisible = this._isVisible(); + const isTouchFriendly = this._isTouchFriendly(); + const $spinButtons = instance.children(); + const spinUp = SpinButton.getInstance($spinButtons.eq(0)); + const spinDown = SpinButton.getInstance($spinButtons.eq(1)); + const options = this._getOptions(); + + spinUp.option(options); + spinDown.option(options); + + this._legacyRender($editor, isTouchFriendly, isVisible); } + } } diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.ts index 64f9b84a5ce0..95ed8a33dc9d 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.ts @@ -1,7 +1,6 @@ -import registerComponent from '../../core/component_registrator'; -import NumberBoxMask from './number_box.mask'; +import registerComponent from '@js/core/component_registrator'; -// STYLE numberBox +import NumberBoxMask from './m_number_box.mask'; registerComponent('dxNumberBox', NumberBoxMask); diff --git a/packages/devextreme/js/__internal/ui/number_box/m_utils.ts b/packages/devextreme/js/__internal/ui/number_box/m_utils.ts index e59e8a398b90..6e568fff2fc4 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_utils.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_utils.ts @@ -1,52 +1,52 @@ -import { adjust } from '../../core/utils/math'; - -const getRealSeparatorIndex = function(str) { - let quoteBalance = 0; - let separatorCount = 0; - - for(let i = 0; i < str.length; ++i) { - if(str[i] === '\'') { - quoteBalance++; - } - if(str[i] === '.') { - ++separatorCount; - if(quoteBalance % 2 === 0) { - return { - occurrence: separatorCount, - index: i - }; - } - } +import { adjust } from '@js/core/utils/math'; + +const getRealSeparatorIndex = function (str) { + let quoteBalance = 0; + let separatorCount = 0; + + for (let i = 0; i < str.length; ++i) { + if (str[i] === '\'') { + quoteBalance++; + } + if (str[i] === '.') { + ++separatorCount; + if (quoteBalance % 2 === 0) { + return { + occurrence: separatorCount, + index: i, + }; + } } + } - return { occurrence: 1, index: -1 }; + return { occurrence: 1, index: -1 }; }; -const getNthOccurrence = function(str, c, n) { - let i = -1; +const getNthOccurrence = function (str, c, n) { + let i = -1; - while(n-- && i++ < str.length) { - i = str.indexOf(c, i); - } + while (n-- && i++ < str.length) { + i = str.indexOf(c, i); + } - return i; + return i; }; -const splitByIndex = function(str, index) { - if(index === -1) { - return [str]; - } +const splitByIndex = function (str, index) { + if (index === -1) { + return [str]; + } - return [str.slice(0, index), str.slice(index + 1)]; + return [str.slice(0, index), str.slice(index + 1)]; }; -const adjustPercentValue = function(rawValue, precision) { - return rawValue && adjust(rawValue / 100, precision); +const adjustPercentValue = function (rawValue, precision) { + return rawValue && adjust(rawValue / 100, precision); }; export { - getRealSeparatorIndex, - getNthOccurrence, - splitByIndex, - adjustPercentValue + adjustPercentValue, + getNthOccurrence, + getRealSeparatorIndex, + splitByIndex, }; diff --git a/packages/devextreme/js/ui/number_box.js b/packages/devextreme/js/ui/number_box.js index ea7e32c1e3a3..f2fd8adba29c 100644 --- a/packages/devextreme/js/ui/number_box.js +++ b/packages/devextreme/js/ui/number_box.js @@ -1,2 +1,40 @@ -import NumberBox from './number_box/number_box'; +import NumberBox from '../__internal/ui/number_box/m_number_box'; + export default NumberBox; + +// STYLE numberBox + +/** + * @name dxNumberBoxOptions.mask + * @hidden + */ + +/** + * @name dxNumberBoxOptions.maskChar + * @hidden + */ + +/** + * @name dxNumberBoxOptions.maskRules + * @hidden + */ + +/** + * @name dxNumberBoxOptions.maskInvalidMessage + * @hidden + */ + +/** + * @name dxNumberBoxOptions.useMaskedValue + * @hidden + */ + +/** + * @name dxNumberBoxOptions.showMaskMode + * @hidden + */ + +/** + * @name dxNumberBoxOptions.spellcheck + * @hidden + */ diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js index e582b6e10a06..38960094bb31 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import SpinButton from 'ui/number_box/number_box.spin'; +import SpinButton from '__internal/ui/number_box/m_number_box.spin'; import config from 'core/config'; import devices from 'core/devices'; import eventsEngine from 'events/core/events_engine'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js index ef326cd39024..546b5dd3d923 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js @@ -1,4 +1,10 @@ -import maskCaret from 'ui/number_box/number_box.caret'; +import { + getCaretWithOffset, + getCaretBoundaries, + getCaretInBoundaries, + isCaretInBoundaries, + getCaretAfterFormat, +} from '__internal/ui/number_box/m_number_box.caret'; QUnit.module('format caret', () => { const customFormat = { @@ -7,79 +13,79 @@ QUnit.module('format caret', () => { }; QUnit.test('getCaretWithOffset', function(assert) { - assert.deepEqual(maskCaret.getCaretWithOffset({ start: 1, end: 2 }, 5), { start: 6, end: 7 }); - assert.deepEqual(maskCaret.getCaretWithOffset({ start: 4, end: 6 }, -2), { start: 2, end: 4 }); - assert.deepEqual(maskCaret.getCaretWithOffset({ start: 4, end: 6 }, -2), { start: 2, end: 4 }); - assert.deepEqual(maskCaret.getCaretWithOffset(5, 1), { start: 6, end: 6 }); + assert.deepEqual(getCaretWithOffset({ start: 1, end: 2 }, 5), { start: 6, end: 7 }); + assert.deepEqual(getCaretWithOffset({ start: 4, end: 6 }, -2), { start: 2, end: 4 }); + assert.deepEqual(getCaretWithOffset({ start: 4, end: 6 }, -2), { start: 2, end: 4 }); + assert.deepEqual(getCaretWithOffset(5, 1), { start: 6, end: 6 }); }); QUnit.test('getCaretBoundaries', function(assert) { - assert.deepEqual(maskCaret.getCaretBoundaries(' # 1,230.45 # ', ' \' # \' #,##0.## \' # \' '), { start: 5, end: 13 }); - assert.deepEqual(maskCaret.getCaretBoundaries('$ 123 tst', '$ #0 tst'), { start: 2, end: 5 }); - assert.deepEqual(maskCaret.getCaretBoundaries('-$ 123 tst', '$ #0 tst'), { start: 3, end: 6 }); - assert.deepEqual(maskCaret.getCaretBoundaries('(($ 123 tst))', '$ #0 tst;(($ #0 tst))'), { start: 4, end: 7 }); - assert.deepEqual(maskCaret.getCaretBoundaries('$ ', '$ #.##'), { start: 2, end: 2 }); - assert.deepEqual(maskCaret.getCaretBoundaries(' kg', '#.## kg'), { start: 0, end: 0 }); - assert.deepEqual(maskCaret.getCaretBoundaries('$ 0.15 ts', '$ #,##0.## ts;($ #,##0.##) ts'), { start: 2, end: 6 }); - assert.deepEqual(maskCaret.getCaretBoundaries('$ 1 ts', '$ #,##0.## ts;($ #,##0.##) ts'), { start: 2, end: 3 }); - assert.deepEqual(maskCaret.getCaretBoundaries('($ 12,345) ts', '$ #,##0.## ts;($ #,##0.##) ts'), { + assert.deepEqual(getCaretBoundaries(' # 1,230.45 # ', ' \' # \' #,##0.## \' # \' '), { start: 5, end: 13 }); + assert.deepEqual(getCaretBoundaries('$ 123 tst', '$ #0 tst'), { start: 2, end: 5 }); + assert.deepEqual(getCaretBoundaries('-$ 123 tst', '$ #0 tst'), { start: 3, end: 6 }); + assert.deepEqual(getCaretBoundaries('(($ 123 tst))', '$ #0 tst;(($ #0 tst))'), { start: 4, end: 7 }); + assert.deepEqual(getCaretBoundaries('$ ', '$ #.##'), { start: 2, end: 2 }); + assert.deepEqual(getCaretBoundaries(' kg', '#.## kg'), { start: 0, end: 0 }); + assert.deepEqual(getCaretBoundaries('$ 0.15 ts', '$ #,##0.## ts;($ #,##0.##) ts'), { start: 2, end: 6 }); + assert.deepEqual(getCaretBoundaries('$ 1 ts', '$ #,##0.## ts;($ #,##0.##) ts'), { start: 2, end: 3 }); + assert.deepEqual(getCaretBoundaries('($ 12,345) ts', '$ #,##0.## ts;($ #,##0.##) ts'), { start: 3, end: 9 }); - assert.deepEqual(maskCaret.getCaretBoundaries('123 mil', customFormat), { + assert.deepEqual(getCaretBoundaries('123 mil', customFormat), { start: 0, end: 7 }); }); QUnit.test('getCaretInBoundaries', function(assert) { - assert.deepEqual(maskCaret.getCaretInBoundaries({ start: 1, end: 5 }, '$ 123', '$ #'), { start: 2, end: 5 }); - assert.deepEqual(maskCaret.getCaretInBoundaries({ start: 1, end: 10 }, '-$ 123 kg', '$ #.## kg'), { + assert.deepEqual(getCaretInBoundaries({ start: 1, end: 5 }, '$ 123', '$ #'), { start: 2, end: 5 }); + assert.deepEqual(getCaretInBoundaries({ start: 1, end: 10 }, '-$ 123 kg', '$ #.## kg'), { start: 3, end: 6 }); }); QUnit.test('isCaretInBoundaries', function(assert) { - assert.equal(maskCaret.isCaretInBoundaries(3, '$ 123 st', '$ #0 st'), true); - assert.equal(maskCaret.isCaretInBoundaries(2, '$ 123 st', '$ #0 st'), true); - assert.equal(maskCaret.isCaretInBoundaries(1, '$ 123 st', '$ #0 st'), false); - assert.equal(maskCaret.isCaretInBoundaries(5, '$ 123 st', '$ #0 st'), true); - assert.equal(maskCaret.isCaretInBoundaries(7, '$ 123 st', '$ #0 st'), false); + assert.equal(isCaretInBoundaries(3, '$ 123 st', '$ #0 st'), true); + assert.equal(isCaretInBoundaries(2, '$ 123 st', '$ #0 st'), true); + assert.equal(isCaretInBoundaries(1, '$ 123 st', '$ #0 st'), false); + assert.equal(isCaretInBoundaries(5, '$ 123 st', '$ #0 st'), true); + assert.equal(isCaretInBoundaries(7, '$ 123 st', '$ #0 st'), false); }); QUnit.test('getCaretAfterFormat with integer part', function(assert) { - assert.deepEqual(maskCaret.getCaretAfterFormat('1234.15', '1,234.15', 2, '#,##0.##'), { start: 3, end: 3 }, 'enter 3 after 2'); - assert.deepEqual(maskCaret.getCaretAfterFormat('(1234.15)', '(1,234.15)', 4, '#,##0.##;(#,##0.##)'), { start: 5, end: 5 }, 'enter 3 after 2 in negative'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1,234.15', '<<1,234.15>>', 3, '#,##0.##;<<#,##0.##>>'), { start: 5, end: 5 }, 'revert sign should save caret position'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1', '$ 1', 1, '$ #'), { start: 3, end: 3 }, 'enter 1 in a blank field'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1,234', '134', 3, '#,##0.##'), { start: 1, end: 1 }, 'remove 2 with delete'); - assert.deepEqual(maskCaret.getCaretAfterFormat(',234', '234', 0, '#,##0.##'), { start: 0, end: 0 }, 'remove 1 with backspace'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1,234', '4', { start: 0, end: 4 }, '#,##0.##'), { start: 0, end: 0 }, 'select and remove some digits'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12534', '1534', 3, '0000'), { start: 2, end: 2 }, 'enter 5 in the middle of decimal format'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12345', '2345', 5, '0000'), { start: 4, end: 4 }, 'enter 5 in the end of decimal format'); + assert.deepEqual(getCaretAfterFormat('1234.15', '1,234.15', 2, '#,##0.##'), { start: 3, end: 3 }, 'enter 3 after 2'); + assert.deepEqual(getCaretAfterFormat('(1234.15)', '(1,234.15)', 4, '#,##0.##;(#,##0.##)'), { start: 5, end: 5 }, 'enter 3 after 2 in negative'); + assert.deepEqual(getCaretAfterFormat('1,234.15', '<<1,234.15>>', 3, '#,##0.##;<<#,##0.##>>'), { start: 5, end: 5 }, 'revert sign should save caret position'); + assert.deepEqual(getCaretAfterFormat('1', '$ 1', 1, '$ #'), { start: 3, end: 3 }, 'enter 1 in a blank field'); + assert.deepEqual(getCaretAfterFormat('1,234', '134', 3, '#,##0.##'), { start: 1, end: 1 }, 'remove 2 with delete'); + assert.deepEqual(getCaretAfterFormat(',234', '234', 0, '#,##0.##'), { start: 0, end: 0 }, 'remove 1 with backspace'); + assert.deepEqual(getCaretAfterFormat('1,234', '4', { start: 0, end: 4 }, '#,##0.##'), { start: 0, end: 0 }, 'select and remove some digits'); + assert.deepEqual(getCaretAfterFormat('12534', '1534', 3, '0000'), { start: 2, end: 2 }, 'enter 5 in the middle of decimal format'); + assert.deepEqual(getCaretAfterFormat('12345', '2345', 5, '0000'), { start: 4, end: 4 }, 'enter 5 in the end of decimal format'); - assert.deepEqual(maskCaret.getCaretAfterFormat('01 mil', '1 mil', 2, customFormat), { start: 1, end: 1 }, 'enter 1 in the end of custom decimal format'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12 mil', '12 mil', 2, customFormat), { start: 2, end: 2 }, 'enter 2 in the end of custom decimal format'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12534 mil', '12534 mil', 3, customFormat), { start: 3, end: 3 }, 'enter 5 in the middle of custom decimal format'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12 mil', '12 mil', 2, customFormat), { start: 2, end: 2 }, 'enter 2 in the end of custom decimal format'); + assert.deepEqual(getCaretAfterFormat('01 mil', '1 mil', 2, customFormat), { start: 1, end: 1 }, 'enter 1 in the end of custom decimal format'); + assert.deepEqual(getCaretAfterFormat('12 mil', '12 mil', 2, customFormat), { start: 2, end: 2 }, 'enter 2 in the end of custom decimal format'); + assert.deepEqual(getCaretAfterFormat('12534 mil', '12534 mil', 3, customFormat), { start: 3, end: 3 }, 'enter 5 in the middle of custom decimal format'); + assert.deepEqual(getCaretAfterFormat('12 mil', '12 mil', 2, customFormat), { start: 2, end: 2 }, 'enter 2 in the end of custom decimal format'); }); QUnit.test('getCaretAfterFormat with float part', function(assert) { - assert.deepEqual(maskCaret.getCaretAfterFormat('1234.00', '1,234', 6, '#,##0.##'), { start: 5, end: 5 }, 'cut zeros in the end'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1.423', '1.42', 3, '#0.00'), { start: 3, end: 3 }, 'enter 4 in the start'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1.243', '1.24', 4, '#0.00'), { start: 4, end: 4 }, 'enter 4 in the middle'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1.234', '1.23', 5, '#0.00'), { start: 4, end: 4 }, 'enter 4 in the end'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1.23', '1.230', 4, '#0.000'), { start: 4, end: 4 }, 'remove 4 with backspace'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1.24', '1.240', 3, '#0.000'), { start: 3, end: 3 }, 'remove 3 with backspace'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1.34', '1.30', 2, '#0.000'), { start: 2, end: 2 }, 'remove 2 with backspace'); - assert.deepEqual(maskCaret.getCaretAfterFormat('1.0000 kg', '1 kg', 6, '#0.### kg'), { start: 1, end: 1 }, 'remove 2 with backspace'); + assert.deepEqual(getCaretAfterFormat('1234.00', '1,234', 6, '#,##0.##'), { start: 5, end: 5 }, 'cut zeros in the end'); + assert.deepEqual(getCaretAfterFormat('1.423', '1.42', 3, '#0.00'), { start: 3, end: 3 }, 'enter 4 in the start'); + assert.deepEqual(getCaretAfterFormat('1.243', '1.24', 4, '#0.00'), { start: 4, end: 4 }, 'enter 4 in the middle'); + assert.deepEqual(getCaretAfterFormat('1.234', '1.23', 5, '#0.00'), { start: 4, end: 4 }, 'enter 4 in the end'); + assert.deepEqual(getCaretAfterFormat('1.23', '1.230', 4, '#0.000'), { start: 4, end: 4 }, 'remove 4 with backspace'); + assert.deepEqual(getCaretAfterFormat('1.24', '1.240', 3, '#0.000'), { start: 3, end: 3 }, 'remove 3 with backspace'); + assert.deepEqual(getCaretAfterFormat('1.34', '1.30', 2, '#0.000'), { start: 2, end: 2 }, 'remove 2 with backspace'); + assert.deepEqual(getCaretAfterFormat('1.0000 kg', '1 kg', 6, '#0.### kg'), { start: 1, end: 1 }, 'remove 2 with backspace'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12.34 mil', '12.34 mil', 2, customFormat), { start: 2, end: 2 }, 'enter 2 before separator'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12.34 mil', '12.34 mil', 4, customFormat), { start: 4, end: 4 }, 'enter 3 after separator'); - assert.deepEqual(maskCaret.getCaretAfterFormat('12.34 mil', '12.34 mil', 5, customFormat), { start: 5, end: 5 }, 'enter 4 in the the end of expression'); + assert.deepEqual(getCaretAfterFormat('12.34 mil', '12.34 mil', 2, customFormat), { start: 2, end: 2 }, 'enter 2 before separator'); + assert.deepEqual(getCaretAfterFormat('12.34 mil', '12.34 mil', 4, customFormat), { start: 4, end: 4 }, 'enter 3 after separator'); + assert.deepEqual(getCaretAfterFormat('12.34 mil', '12.34 mil', 5, customFormat), { start: 5, end: 5 }, 'enter 4 in the the end of expression'); - assert.deepEqual(maskCaret.getCaretAfterFormat('. 1.50', '. 1.350', 5, '\'.\' 0.00'), { start: 5, end: 5 }, 'enter 3 after separator'); + assert.deepEqual(getCaretAfterFormat('. 1.50', '. 1.350', 5, '\'.\' 0.00'), { start: 5, end: 5 }, 'enter 3 after separator'); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/rangeSlider.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/rangeSlider.tests.js index 1563db0db232..87c4662ccd2c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/rangeSlider.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/rangeSlider.tests.js @@ -5,8 +5,8 @@ import pointerMock from '../../helpers/pointerMock.js'; import fx from 'animation/fx'; import { normalizeKeyName } from 'events/utils/index'; -import '__internal/ui/m_range_slider'; -import 'ui/number_box/number_box'; +import 'ui/range_slider'; +import 'ui/number_box'; import 'ui/validator'; import 'generic_light.css!'; From 3266b53434a337f2ec29cba55110823df2c49b3f Mon Sep 17 00:00:00 2001 From: Vlada Skorohodova <94827090+vladaskorohodova@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:50:49 +0400 Subject: [PATCH 13/32] DropDownBox: update demo descriptions (#27546) --- .../DropDownBox/MultipleSelection/Angular/description.md | 4 ---- .../DropDownBox/MultipleSelection/React/description.md | 4 ---- .../DropDownBox/MultipleSelection/ReactJs/description.md | 4 ---- .../DropDownBox/MultipleSelection/Vue/description.md | 4 ---- .../Demos/DropDownBox/MultipleSelection/description.md | 8 ++++++++ .../DropDownBox/MultipleSelection/jQuery/description.md | 4 ---- .../Demos/DropDownBox/SingleSelection/description.md | 6 +++++- 7 files changed, 13 insertions(+), 21 deletions(-) delete mode 100644 apps/demos/Demos/DropDownBox/MultipleSelection/Angular/description.md delete mode 100644 apps/demos/Demos/DropDownBox/MultipleSelection/React/description.md delete mode 100644 apps/demos/Demos/DropDownBox/MultipleSelection/ReactJs/description.md delete mode 100644 apps/demos/Demos/DropDownBox/MultipleSelection/Vue/description.md delete mode 100644 apps/demos/Demos/DropDownBox/MultipleSelection/jQuery/description.md diff --git a/apps/demos/Demos/DropDownBox/MultipleSelection/Angular/description.md b/apps/demos/Demos/DropDownBox/MultipleSelection/Angular/description.md deleted file mode 100644 index 7fc5fef756e0..000000000000 --- a/apps/demos/Demos/DropDownBox/MultipleSelection/Angular/description.md +++ /dev/null @@ -1,4 +0,0 @@ -If the UI component embedded into the DropDownBox allows multiple selection, synchronize the DropDownBox value with the selected items. [Synchronization instructions](/Demos/WidgetsGallery/Demo/DropDownBox/SingleSelection/Angular/Light/) are the same for every selection mode. - - -In this demo, the DataGrid's **selection**.[mode](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode) and TreeView's [selectionMode](/Documentation/ApiReference/UI_Components/dxTreeView/Configuration/#selectionMode) properties are used to enable multiple selection. \ No newline at end of file diff --git a/apps/demos/Demos/DropDownBox/MultipleSelection/React/description.md b/apps/demos/Demos/DropDownBox/MultipleSelection/React/description.md deleted file mode 100644 index 66c4e23cf05e..000000000000 --- a/apps/demos/Demos/DropDownBox/MultipleSelection/React/description.md +++ /dev/null @@ -1,4 +0,0 @@ -If the UI component embedded into the DropDownBox allows multiple selection, synchronize the DropDownBox value with the selected items. [Synchronization instructions](/Demos/WidgetsGallery/Demo/DropDownBox/SingleSelection/React/Light/) are the same for every selection mode. - - -In this demo, the DataGrid's **selection**.[mode](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode) and TreeView's [selectionMode](/Documentation/ApiReference/UI_Components/dxTreeView/Configuration/#selectionMode) properties are used to enable multiple selection. \ No newline at end of file diff --git a/apps/demos/Demos/DropDownBox/MultipleSelection/ReactJs/description.md b/apps/demos/Demos/DropDownBox/MultipleSelection/ReactJs/description.md deleted file mode 100644 index 66c4e23cf05e..000000000000 --- a/apps/demos/Demos/DropDownBox/MultipleSelection/ReactJs/description.md +++ /dev/null @@ -1,4 +0,0 @@ -If the UI component embedded into the DropDownBox allows multiple selection, synchronize the DropDownBox value with the selected items. [Synchronization instructions](/Demos/WidgetsGallery/Demo/DropDownBox/SingleSelection/React/Light/) are the same for every selection mode. - - -In this demo, the DataGrid's **selection**.[mode](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode) and TreeView's [selectionMode](/Documentation/ApiReference/UI_Components/dxTreeView/Configuration/#selectionMode) properties are used to enable multiple selection. \ No newline at end of file diff --git a/apps/demos/Demos/DropDownBox/MultipleSelection/Vue/description.md b/apps/demos/Demos/DropDownBox/MultipleSelection/Vue/description.md deleted file mode 100644 index c280d1abf178..000000000000 --- a/apps/demos/Demos/DropDownBox/MultipleSelection/Vue/description.md +++ /dev/null @@ -1,4 +0,0 @@ -If the UI component embedded into the DropDownBox allows multiple selection, synchronize the DropDownBox value with the selected items. [Synchronization instructions](/Demos/WidgetsGallery/Demo/DropDownBox/SingleSelection/Vue/Light/) are the same for every selection mode. - - -In this demo, the DataGrid's **selection**.[mode](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode) and TreeView's [selectionMode](/Documentation/ApiReference/UI_Components/dxTreeView/Configuration/#selectionMode) properties are used to enable multiple selection. \ No newline at end of file diff --git a/apps/demos/Demos/DropDownBox/MultipleSelection/description.md b/apps/demos/Demos/DropDownBox/MultipleSelection/description.md index e69de29bb2d1..0ef8ebb7ae60 100644 --- a/apps/demos/Demos/DropDownBox/MultipleSelection/description.md +++ b/apps/demos/Demos/DropDownBox/MultipleSelection/description.md @@ -0,0 +1,8 @@ +DropDownBox is an advanced editor whose drop-down window can include other components. + +DevExtreme ships with multiple other drop-down editors. To find out which editor best suits your task, review the following article: [How to Choose a Drop-Down Editor](/Documentation/Guide/UI_Components/Lookup/Choose_a_Drop-Down_Editor/). + +If the UI component embedded into the DropDownBox component's drop-down window allows multiple selection, synchronize the component's value with the state of selected items as demonstrated in the following demo: [Single Selection](/Demos/WidgetsGallery/Demo/DropDownBox/SingleSelection/). + + +This demo uses the DataGrid's **selection**.[mode](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode) and TreeView's [selectionMode](/Documentation/ApiReference/UI_Components/dxTreeView/Configuration/#selectionMode) properties to enable multiple selection. \ No newline at end of file diff --git a/apps/demos/Demos/DropDownBox/MultipleSelection/jQuery/description.md b/apps/demos/Demos/DropDownBox/MultipleSelection/jQuery/description.md deleted file mode 100644 index e2c663e2f5d9..000000000000 --- a/apps/demos/Demos/DropDownBox/MultipleSelection/jQuery/description.md +++ /dev/null @@ -1,4 +0,0 @@ -If the UI component embedded into the DropDownBox allows multiple selection, synchronize the DropDownBox value with the selected items. [Synchronization instructions](/Demos/WidgetsGallery/Demo/DropDownBox/SingleSelection/jQuery/Light/) are the same for every selection mode. - - -In this demo, the DataGrid's **selection**.[mode](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode) and TreeView's [selectionMode](/Documentation/ApiReference/UI_Components/dxTreeView/Configuration/#selectionMode) properties are used to enable multiple selection. \ No newline at end of file diff --git a/apps/demos/Demos/DropDownBox/SingleSelection/description.md b/apps/demos/Demos/DropDownBox/SingleSelection/description.md index 33b30e90f529..e319b0208238 100644 --- a/apps/demos/Demos/DropDownBox/SingleSelection/description.md +++ b/apps/demos/Demos/DropDownBox/SingleSelection/description.md @@ -1,8 +1,12 @@ -The DropDownBox component consists of a text field and drop-down content. In this demo, the content is the TreeView and the DataGrid in single selection mode. +DropDownBox is an advanced editor whose drop-down window can include other components. + +DevExtreme ships with multiple other drop-down editors. To find out which editor best suits your task, review the following article: [How to Choose a Drop-Down Editor](/Documentation/Guide/UI_Components/Lookup/Choose_a_Drop-Down_Editor/). To get started with the DevExtreme DropDownBox component, refer to the following step-by-step tutorial: [Getting Started with DropDownBox](/Documentation/Guide/UI_Components/DropDownBox/Getting_Started_with_DropDownBox/). +In this demo, TreeView and the DataGrid components in single-selection mode are embedded into the DropDownBox window. + The following instructions show how to synchronize the DropDownBox with an embedded DevExtreme component: 1. **Specify data sources** From 71f4a37acffa496789a35d4d75207e35754dd142 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 6 Jun 2024 10:22:34 +0400 Subject: [PATCH 14/32] ToolbarBase, Toolbar: move files to TS --- .../ui/toolbar/internal/m_toolbar.menu.list.ts} | 0 .../ui/toolbar/internal/m_toolbar.menu.ts} | 0 .../toolbar/constants.js => __internal/ui/toolbar/m_constants.ts} | 0 .../ui/toolbar/m_toolbar.base.ts} | 0 .../toolbar/ui.toolbar.js => __internal/ui/toolbar/m_toolbar.ts} | 0 .../ui/toolbar/m_toolbar.utils.ts} | 0 .../ui/toolbar/strategy/m_toolbar.multiline.ts} | 0 .../ui/toolbar/strategy/m_toolbar.singleline.ts} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/toolbar/internal/ui.toolbar.menu.list.js => __internal/ui/toolbar/internal/m_toolbar.menu.list.ts} (100%) rename packages/devextreme/js/{ui/toolbar/internal/ui.toolbar.menu.js => __internal/ui/toolbar/internal/m_toolbar.menu.ts} (100%) rename packages/devextreme/js/{ui/toolbar/constants.js => __internal/ui/toolbar/m_constants.ts} (100%) rename packages/devextreme/js/{ui/toolbar/ui.toolbar.base.js => __internal/ui/toolbar/m_toolbar.base.ts} (100%) rename packages/devextreme/js/{ui/toolbar/ui.toolbar.js => __internal/ui/toolbar/m_toolbar.ts} (100%) rename packages/devextreme/js/{ui/toolbar/ui.toolbar.utils.js => __internal/ui/toolbar/m_toolbar.utils.ts} (100%) rename packages/devextreme/js/{ui/toolbar/strategy/toolbar.multiline.js => __internal/ui/toolbar/strategy/m_toolbar.multiline.ts} (100%) rename packages/devextreme/js/{ui/toolbar/strategy/toolbar.singleline.js => __internal/ui/toolbar/strategy/m_toolbar.singleline.ts} (100%) diff --git a/packages/devextreme/js/ui/toolbar/internal/ui.toolbar.menu.list.js b/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.list.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/internal/ui.toolbar.menu.list.js rename to packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.list.ts diff --git a/packages/devextreme/js/ui/toolbar/internal/ui.toolbar.menu.js b/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/internal/ui.toolbar.menu.js rename to packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.ts diff --git a/packages/devextreme/js/ui/toolbar/constants.js b/packages/devextreme/js/__internal/ui/toolbar/m_constants.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/constants.js rename to packages/devextreme/js/__internal/ui/toolbar/m_constants.ts diff --git a/packages/devextreme/js/ui/toolbar/ui.toolbar.base.js b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.base.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/ui.toolbar.base.js rename to packages/devextreme/js/__internal/ui/toolbar/m_toolbar.base.ts diff --git a/packages/devextreme/js/ui/toolbar/ui.toolbar.js b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/ui.toolbar.js rename to packages/devextreme/js/__internal/ui/toolbar/m_toolbar.ts diff --git a/packages/devextreme/js/ui/toolbar/ui.toolbar.utils.js b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.utils.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/ui.toolbar.utils.js rename to packages/devextreme/js/__internal/ui/toolbar/m_toolbar.utils.ts diff --git a/packages/devextreme/js/ui/toolbar/strategy/toolbar.multiline.js b/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.multiline.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/strategy/toolbar.multiline.js rename to packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.multiline.ts diff --git a/packages/devextreme/js/ui/toolbar/strategy/toolbar.singleline.js b/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.singleline.ts similarity index 100% rename from packages/devextreme/js/ui/toolbar/strategy/toolbar.singleline.js rename to packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.singleline.ts From ff79d2b0860207667e242af8e213ee90f5ddb194 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 6 Jun 2024 11:02:16 +0400 Subject: [PATCH 15/32] ToolbarBase, Toolbar, ToolbarMenu: ignore errors after move to TS --- .../js/__internal/ui/collection/async.ts | 20 + .../js/__internal/ui/collection/base.ts | 9 +- .../ui/context_menu/m_context_menu.ts | 1 - .../__internal/ui/context_menu/m_menu_base.ts | 2 +- .../js/__internal/ui/form/m_form.ts | 2 +- .../js/__internal/ui/menu/m_menu.ts | 3 +- .../toolbar/internal/m_toolbar.menu.list.ts | 187 ++-- .../ui/toolbar/internal/m_toolbar.menu.ts | 667 +++++++------- .../__internal/ui/toolbar/m_toolbar.base.ts | 845 ++++++++++-------- .../js/__internal/ui/toolbar/m_toolbar.ts | 329 +++---- .../__internal/ui/toolbar/m_toolbar.utils.ts | 75 +- .../toolbar/strategy/m_toolbar.multiline.ts | 59 +- .../toolbar/strategy/m_toolbar.singleline.ts | 430 ++++----- .../devextreme/js/__internal/ui/widget.ts | 14 +- .../js/renovation/ui/common/core.d.ts | 4 +- packages/devextreme/js/ui/popup/ui.popup.js | 2 +- packages/devextreme/js/ui/toolbar.js | 72 ++ .../js/ui/toolbar/ui.toolbar.base.js | 3 + .../devextreme/js/ui/toolbar/ui.toolbar.js | 3 + .../DevExpress.ui.widgets.form/form.tests.js | 2 +- .../toolbar.menu.tests.js | 4 +- .../toolbar.multiline.tests.js | 4 +- .../DevExpress.ui.widgets/toolbar.tests.js | 1 + .../DevExpress.ui/defaultOptions.tests.js | 2 +- 24 files changed, 1466 insertions(+), 1274 deletions(-) create mode 100644 packages/devextreme/js/__internal/ui/collection/async.ts create mode 100644 packages/devextreme/js/ui/toolbar/ui.toolbar.base.js create mode 100644 packages/devextreme/js/ui/toolbar/ui.toolbar.js diff --git a/packages/devextreme/js/__internal/ui/collection/async.ts b/packages/devextreme/js/__internal/ui/collection/async.ts new file mode 100644 index 000000000000..b7b88bfcab67 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/collection/async.ts @@ -0,0 +1,20 @@ +import CollectionWidgetAsync from '@js/ui/collection/ui.collection_widget.async'; +import type { CollectionWidgetOptions, ItemLike } from '@js/ui/collection/ui.collection_widget.base'; + +import CollectionWidgetEdit from './edit'; + +declare class Async< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TProperties extends CollectionWidgetOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends ItemLike = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, +> extends CollectionWidgetEdit { + _renderItemsAsync(): void; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const TypedCollectionWidget: typeof Async = CollectionWidgetAsync as any; + +export default TypedCollectionWidget; diff --git a/packages/devextreme/js/__internal/ui/collection/base.ts b/packages/devextreme/js/__internal/ui/collection/base.ts index 9f76624faf73..deb3dd9d0f36 100644 --- a/packages/devextreme/js/__internal/ui/collection/base.ts +++ b/packages/devextreme/js/__internal/ui/collection/base.ts @@ -24,6 +24,8 @@ declare class Base< // eslint-disable-next-line @typescript-eslint/no-explicit-any TKey = any, > extends Widget { + _renderedItemsCount: number; + getDataSource(): DataSource; _renderItems(items: TItem[]): void; @@ -61,7 +63,12 @@ declare class Base< _getIndexByItem(item: TItem): number; _getIndexByItemData(item: TItem): number; _getAvailableItems($items?: dxElementWrapper): dxElementWrapper; - _getSummaryItemsSize(dimension: string, items: TItem[], includeMargin: boolean): number; + _itemElements(): dxElementWrapper; + _getSummaryItemsSize( + dimension: string, + items: dxElementWrapper, + includeMargin?: boolean + ): number; _getActiveItem(last?: boolean): dxElementWrapper; _findItemElementByItem(item: TItem): dxElementWrapper; diff --git a/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts b/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts index e0e12c0c9910..cbd0df7fabc1 100644 --- a/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts +++ b/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts @@ -669,7 +669,6 @@ class ContextMenu extends MenuBase { } _initScrollable($container) { - // @ts-expect-error this._createComponent($container, Scrollable, { useKeyboard: false, _onVisibilityChanged: (scrollable) => { diff --git a/packages/devextreme/js/__internal/ui/context_menu/m_menu_base.ts b/packages/devextreme/js/__internal/ui/context_menu/m_menu_base.ts index a86d1713880e..bb09e1d9047b 100644 --- a/packages/devextreme/js/__internal/ui/context_menu/m_menu_base.ts +++ b/packages/devextreme/js/__internal/ui/context_menu/m_menu_base.ts @@ -661,8 +661,8 @@ class MenuBase extends HierarchicalCollectionWidget { _getElementByItem(itemData) { let result; - // @ts-expect-error each(this._itemElements(), (_, itemElement) => { + // @ts-expect-error if ($(itemElement).data(this._itemDataKey()) !== itemData) { return true; } diff --git a/packages/devextreme/js/__internal/ui/form/m_form.ts b/packages/devextreme/js/__internal/ui/form/m_form.ts index 433d4ab64eae..574e91aae1fe 100644 --- a/packages/devextreme/js/__internal/ui/form/m_form.ts +++ b/packages/devextreme/js/__internal/ui/form/m_form.ts @@ -24,9 +24,9 @@ import Editor from '@js/ui/editor/editor'; import Scrollable from '@js/ui/scroll_view/ui.scrollable'; import TabPanel from '@js/ui/tab_panel'; import { isMaterial, isMaterialBased } from '@js/ui/themes'; -import { TOOLBAR_CLASS } from '@js/ui/toolbar/constants'; import ValidationEngine from '@js/ui/validation_engine'; import Widget from '@js/ui/widget/ui.widget'; +import { TOOLBAR_CLASS } from '@ts/ui/toolbar/m_constants'; import { setLabelWidthByMaxLabelWidth, diff --git a/packages/devextreme/js/__internal/ui/menu/m_menu.ts b/packages/devextreme/js/__internal/ui/menu/m_menu.ts index 8128f2b4aae0..09a341f9ea83 100644 --- a/packages/devextreme/js/__internal/ui/menu/m_menu.ts +++ b/packages/devextreme/js/__internal/ui/menu/m_menu.ts @@ -116,10 +116,9 @@ class Menu extends MenuBase { } _itemElements() { - // @ts-expect-error const rootMenuElements = super._itemElements(); const submenuElements = this._submenuItemElements(); - + // @ts-expect-error return rootMenuElements.add(submenuElements); } diff --git a/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.list.ts b/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.list.ts index 8d875fae709f..185ccf1532ba 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.list.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.list.ts @@ -1,6 +1,6 @@ -import $ from '../../../core/renderer'; -import { each } from '../../../core/utils/iterator'; -import { ListBase } from '../../list/ui.list.base'; +import $ from '@js/core/renderer'; +import { each } from '@js/core/utils/iterator'; +import { ListBase } from '@js/ui/list/ui.list.base'; const TOOLBAR_MENU_ACTION_CLASS = 'dx-toolbar-menu-action'; const TOOLBAR_HIDDEN_BUTTON_CLASS = 'dx-toolbar-hidden-button'; @@ -11,107 +11,114 @@ const TOOLBAR_MENU_LAST_SECTION_CLASS = 'dx-toolbar-menu-last-section'; const SCROLLVIEW_CONTENT_CLASS = 'dx-scrollview-content'; export default class ToolbarMenuList extends ListBase { - _init() { - super._init(); - - this._activeStateUnit = `.${TOOLBAR_MENU_ACTION_CLASS}:not(.${TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS})`; - } - - _initMarkup() { - this._renderSections(); - super._initMarkup(); - this._setMenuRole(); - } - - _getSections() { - return this._itemContainer().children(); - } - - _itemElements() { - return this._getSections().children(this._itemSelector()); - } - - _renderSections() { - const $container = this._itemContainer(); - - each(['before', 'center', 'after', 'menu'], (_, section) => { - const sectionName = `_$${section}Section`; - - if(!this[sectionName]) { - this[sectionName] = $('
') - .addClass(TOOLBAR_MENU_SECTION_CLASS); - } - - this[sectionName].appendTo($container); - }); + _activeStateUnit?: string; + + _init(): void { + super._init(); + + this._activeStateUnit = `.${TOOLBAR_MENU_ACTION_CLASS}:not(.${TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS})`; + } + + _initMarkup(): void { + this._renderSections(); + super._initMarkup(); + this._setMenuRole(); + } + + _getSections() { + // @ts-expect-error + return this._itemContainer().children(); + } + + _itemElements() { + // @ts-expect-error + return this._getSections().children(this._itemSelector()); + } + + _renderSections(): void { + // @ts-expect-error + const $container = this._itemContainer(); + + each(['before', 'center', 'after', 'menu'], (_, section) => { + const sectionName = `_$${section}Section`; + + if (!this[sectionName]) { + this[sectionName] = $('
') + .addClass(TOOLBAR_MENU_SECTION_CLASS); + } + + this[sectionName].appendTo($container); + }); + } + + _renderItems(): void { + super._renderItems.apply(this, arguments); + this._updateSections(); + } + + _setMenuRole(): void { + // @ts-expect-error + const $menuContainer = this.$element().find(`.${SCROLLVIEW_CONTENT_CLASS}`); + + $menuContainer.attr('role', 'menu'); + } + + _updateSections(): void { + // @ts-expect-error + const $sections = this.$element().find(`.${TOOLBAR_MENU_SECTION_CLASS}`); + $sections.removeClass(TOOLBAR_MENU_LAST_SECTION_CLASS); + $sections.not(':empty').eq(-1).addClass(TOOLBAR_MENU_LAST_SECTION_CLASS); + } + + _renderItem(index, item, itemContainer, $after) { + const location = item.location ?? 'menu'; + const $container = this[`_$${location}Section`]; + const itemElement = super._renderItem(index, item, $container, $after); + + if (this._getItemTemplateName({ itemData: item })) { + itemElement.addClass(TOOLBAR_MENU_CUSTOM_CLASS); } - _renderItems() { - super._renderItems.apply(this, arguments); - this._updateSections(); + if (location === 'menu' || item.widget === 'dxButton' || item.widget === 'dxButtonGroup' || item.isAction) { + itemElement.addClass(TOOLBAR_MENU_ACTION_CLASS); } - _setMenuRole() { - const $menuContainer = this.$element().find(`.${SCROLLVIEW_CONTENT_CLASS}`); - - $menuContainer.attr('role', 'menu'); + if (item.widget === 'dxButton') { + itemElement.addClass(TOOLBAR_HIDDEN_BUTTON_CLASS); } - _updateSections() { - const $sections = this.$element().find(`.${TOOLBAR_MENU_SECTION_CLASS}`); - $sections.removeClass(TOOLBAR_MENU_LAST_SECTION_CLASS); - $sections.not(':empty').eq(-1).addClass(TOOLBAR_MENU_LAST_SECTION_CLASS); + if (item.widget === 'dxButtonGroup') { + itemElement.addClass(TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS); } - _renderItem(index, item, itemContainer, $after) { - const location = item.location ?? 'menu'; - const $container = this[`_$${location}Section`]; - const itemElement = super._renderItem(index, item, $container, $after); - - if(this._getItemTemplateName({ itemData: item })) { - itemElement.addClass(TOOLBAR_MENU_CUSTOM_CLASS); - } + itemElement.addClass(item.cssClass); - if(location === 'menu' || item.widget === 'dxButton' || item.widget === 'dxButtonGroup' || item.isAction) { - itemElement.addClass(TOOLBAR_MENU_ACTION_CLASS); - } + return itemElement; + } - if(item.widget === 'dxButton') { - itemElement.addClass(TOOLBAR_HIDDEN_BUTTON_CLASS); - } + _getItemTemplateName(args) { + const template = super._getItemTemplateName(args); - if(item.widget === 'dxButtonGroup') { - itemElement.addClass(TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS); - } - - itemElement.addClass(item.cssClass); - - return itemElement; - } + const data = args.itemData; + const menuTemplate = data && data.menuItemTemplate; - _getItemTemplateName(args) { - const template = super._getItemTemplateName(args); + return menuTemplate || template; + } - const data = args.itemData; - const menuTemplate = data && data['menuItemTemplate']; + _dataSourceOptions() { + return { + paginate: false, + }; + } - return menuTemplate || template; + _itemClickHandler(e, args, config): void { + if ($(e.target).closest(`.${TOOLBAR_MENU_ACTION_CLASS}`).length) { + super._itemClickHandler(e, args, config); } + } - _dataSourceOptions() { - return { - paginate: false - }; - } - - _itemClickHandler(e, args, config) { - if($(e.target).closest(`.${TOOLBAR_MENU_ACTION_CLASS}`).length) { - super._itemClickHandler(e, args, config); - } - } - - _clean() { - this._getSections().empty(); - super._clean(); - } + _clean(): void { + this._getSections().empty(); + super._clean(); + } } diff --git a/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.ts b/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.ts index a63736c0ce43..dc126e65d3b8 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/internal/m_toolbar.menu.ts @@ -1,16 +1,23 @@ -import { getOuterHeight } from '../../../core/utils/size'; -import $ from '../../../core/renderer'; -import devices from '../../../core/devices'; -import { extend } from '../../../core/utils/extend'; -import Widget from '../../widget/ui.widget'; -import Button from '../../button'; -import ToolbarMenuList from './ui.toolbar.menu.list'; -import { isFluent, isMaterialBased } from '../../themes'; -import { ChildDefaultTemplate } from '../../../core/templates/child_default_template'; -import { toggleItemFocusableElementTabIndex } from '../ui.toolbar.utils'; -import { getWindow } from '../../../core/utils/window'; - -import '../../popup'; +import '@js/ui/popup'; + +import devices from '@js/core/devices'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { ChildDefaultTemplate } from '@js/core/templates/child_default_template'; +import { extend } from '@js/core/utils/extend'; +import { getOuterHeight } from '@js/core/utils/size'; +import { getWindow } from '@js/core/utils/window'; +import Button from '@js/ui/button'; +import type dxList from '@js/ui/list'; +import type { dxPopupAnimation } from '@js/ui/popup'; +import type Popup from '@js/ui/popup'; +import { isFluent, isMaterialBased } from '@js/ui/themes'; +import type { Item } from '@js/ui/toolbar'; +import type { WidgetOptions } from '@js/ui/widget/ui.widget'; +import Widget from '@ts/ui/widget'; + +import { toggleItemFocusableElementTabIndex } from '../m_toolbar.utils'; +import ToolbarMenuList from './m_toolbar.menu.list'; const DROP_DOWN_MENU_CLASS = 'dx-dropdownmenu'; const DROP_DOWN_MENU_POPUP_CLASS = 'dx-dropdownmenu-popup'; @@ -20,326 +27,368 @@ const DROP_DOWN_MENU_BUTTON_CLASS = 'dx-dropdownmenu-button'; const POPUP_BOUNDARY_VERTICAL_OFFSET = 10; const POPUP_VERTICAL_OFFSET = 3; -export default class DropDownMenu extends Widget { - _supportedKeys() { - let extension = {}; +export interface Properties extends WidgetOptions { + opened?: boolean; - if(!this.option('opened') || !this._list.option('focusedElement')) { - extension = this._button._supportedKeys(); - } - - return extend(super._supportedKeys(), extension, { - tab: function() { - this._popup && this._popup.hide(); - } - }); - } - - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - items: [], - onItemClick: null, - dataSource: null, - itemTemplate: 'item', - onButtonClick: null, - activeStateEnabled: true, - hoverStateEnabled: true, - opened: false, - onItemRendered: null, - closeOnClick: true, - useInkRipple: false, - container: undefined, - animation: { - show: { type: 'fade', from: 0, to: 1 }, - hide: { type: 'fade', to: 0 } - } - }); - } - - _defaultOptionsRules() { - return super._defaultOptionsRules().concat([ - { - device: function() { - return devices.real().deviceType === 'desktop' && !devices.isSimulator(); - }, - options: { - focusStateEnabled: true - } - }, - { - device: function() { - return isMaterialBased(); - }, - options: { - useInkRipple: true, - animation: { - show: { - type: 'pop', - duration: 200, - from: { scale: 0 }, - to: { scale: 1 } - }, - hide: { - type: 'pop', - duration: 200, - from: { scale: 1 }, - to: { scale: 0 } - } - } - } - }, - ]); - } - - _init() { - super._init(); - - this.$element().addClass(DROP_DOWN_MENU_CLASS); - - this._initItemClickAction(); - this._initButtonClickAction(); - } + container?: string | Element | undefined; - _initItemClickAction() { - this._itemClickAction = this._createActionByOption('onItemClick'); - } + animation?: dxPopupAnimation; - _initButtonClickAction() { - this._buttonClickAction = this._createActionByOption('onButtonClick'); - } + items?: Item[]; +} - _initTemplates() { - this._templateManager.addDefaultTemplates({ - content: new ChildDefaultTemplate('content') - }); - super._initTemplates(); - } +export default class DropDownMenu extends Widget { + _button?: Button; - _initMarkup() { - this._renderButton(); - super._initMarkup(); - } + _popup?: Popup; - _render() { - super._render(); - this.setAria({ - 'haspopup': true, - 'expanded': this.option('opened') - }); - } + _list?: dxList; - _renderContentImpl() { - if(this.option('opened')) { - this._renderPopup(); - } - } + _$popup?: dxElementWrapper; - _clean() { - this._cleanFocusState(); + _deferRendering?: boolean; - this._list && this._list.$element().remove(); - this._popup && this._popup.$element().remove(); + _itemClickAction?: any; - delete this._list; - delete this._popup; - } + _buttonClickAction?: any; - _renderButton() { - const $button = this.$element().addClass(DROP_DOWN_MENU_BUTTON_CLASS); - - this._button = this._createComponent($button, Button, { - icon: 'overflow', - template: 'content', - stylingMode: isFluent() ? 'text' : 'contained', - useInkRipple: this.option('useInkRipple'), - hoverStateEnabled: false, - focusStateEnabled: false, - onClick: (e) => { - this.option('opened', !this.option('opened')); - this._buttonClickAction(e); - } - }); - } + _supportedKeys() { + let extension = {}; - _toggleActiveState($element, value, e) { - this._button._toggleActiveState($element, value, e); + if (!this.option('opened') || !this._list?.option('focusedElement')) { + // @ts-expect-error + extension = this._button._supportedKeys(); } - _toggleMenuVisibility(opened) { - const state = opened ?? !this._popup.option('visible'); - - if(opened) { - this._renderPopup(); - } - - this._popup.toggle(state); - this.setAria('expanded', state); - } - - _renderPopup() { - if(this._$popup) { - return; - } - - this._$popup = $('
').appendTo(this.$element()); - const { rtlEnabled, container, animation } = this.option(); - - this._popup = this._createComponent(this._$popup, 'dxPopup', { - onInitialized({ component }) { - component.$wrapper() - .addClass(DROP_DOWN_MENU_POPUP_WRAPPER_CLASS) - .addClass(DROP_DOWN_MENU_POPUP_CLASS); + return extend(super._supportedKeys(), extension, { + tab() { + this._popup && this._popup.hide(); + }, + }); + } + + _getDefaultOptions(): Properties { + return { + ...super._getDefaultOptions(), + items: [], + // @ts-expect-error + onItemClick: null, + dataSource: null, + itemTemplate: 'item', + onButtonClick: null, + activeStateEnabled: true, + hoverStateEnabled: true, + opened: false, + onItemRendered: null, + closeOnClick: true, + useInkRipple: false, + container: undefined, + animation: { + show: { type: 'fade', from: 0, to: 1 }, + hide: { type: 'fade', to: 0 }, + }, + }; + } + + _defaultOptionsRules() { + return super._defaultOptionsRules().concat([ + { + device() { + return devices.real().deviceType === 'desktop' && !devices.isSimulator(); + }, + options: { + focusStateEnabled: true, + }, + }, + { + device() { + // @ts-expect-error + return isMaterialBased(); + }, + options: { + useInkRipple: true, + animation: { + show: { + type: 'pop', + duration: 200, + from: { scale: 0 }, + to: { scale: 1 }, }, - deferRendering: false, - contentTemplate: (contentElement) => this._renderList(contentElement), - _ignoreFunctionValueDeprecation: true, - maxHeight: () => this._getMaxHeight(), - position: { - my: `top ${rtlEnabled ? 'left' : 'right'}`, - at: `bottom ${rtlEnabled ? 'left' : 'right'}`, - collision: 'fit flip', - offset: { v: POPUP_VERTICAL_OFFSET }, - of: this.$element() + hide: { + type: 'pop', + duration: 200, + from: { scale: 1 }, + to: { scale: 0 }, }, - animation, - onOptionChanged: ({ name, value }) => { - if(name === 'visible') { - this.option('opened', value); - } - }, - container, - autoResizeEnabled: false, - height: 'auto', - width: 'auto', - hideOnOutsideClick: (e) => this._closeOutsideDropDownHandler(e), - hideOnParentScroll: true, - shading: false, - dragEnabled: false, - showTitle: false, - fullScreen: false, - _fixWrapperPosition: true, - }); - } - - _getMaxHeight() { - const $element = this.$element(); - - const offsetTop = $element.offset().top; - const windowHeight = getOuterHeight(getWindow()); - const maxHeight = Math.max(offsetTop, windowHeight - offsetTop - getOuterHeight($element)); - - return Math.min(windowHeight, maxHeight - POPUP_VERTICAL_OFFSET - POPUP_BOUNDARY_VERTICAL_OFFSET); + }, + }, + }, + ]); + } + + _init(): void { + super._init(); + + // @ts-expect-error + this.$element().addClass(DROP_DOWN_MENU_CLASS); + + this._initItemClickAction(); + this._initButtonClickAction(); + } + + _initItemClickAction() { + this._itemClickAction = this._createActionByOption('onItemClick'); + } + + _initButtonClickAction() { + this._buttonClickAction = this._createActionByOption('onButtonClick'); + } + + _initTemplates() { + this._templateManager.addDefaultTemplates({ + content: new ChildDefaultTemplate('content'), + }); + super._initTemplates(); + } + + _initMarkup(): void { + this._renderButton(); + super._initMarkup(); + } + + _render(): void { + super._render(); + this.setAria({ + haspopup: true, + expanded: this.option('opened'), + }); + } + + _renderContentImpl(): void { + if (this.option('opened')) { + this._renderPopup(); } - - _closeOutsideDropDownHandler(e) { - const isOutsideClick = !$(e.target).closest(this.$element()).length; - - return isOutsideClick; + } + + _clean(): void { + this._cleanFocusState(); + + this._list && this._list.$element().remove(); + this._popup && this._popup.$element().remove(); + + delete this._list; + delete this._popup; + } + + _renderButton(): void { + // @ts-expect-error + const $button = this.$element().addClass(DROP_DOWN_MENU_BUTTON_CLASS); + + this._button = this._createComponent($button, Button, { + icon: 'overflow', + template: 'content', + // @ts-expect-error + stylingMode: isFluent() ? 'text' : 'contained', + // @ts-expect-error + useInkRipple: this.option('useInkRipple'), + hoverStateEnabled: false, + focusStateEnabled: false, + onClick: (e) => { + this.option('opened', !this.option('opened')); + this._buttonClickAction(e); + }, + }); + } + + _toggleActiveState($element, value, e): void { + // @ts-expect-error + this._button._toggleActiveState($element, value, e); + } + + _toggleMenuVisibility(opened) { + const state = opened ?? !this._popup?.option('visible'); + + if (opened) { + this._renderPopup(); } - _renderList(contentElement) { - const $content = $(contentElement); - $content.addClass(DROP_DOWN_MENU_LIST_CLASS); - - this._list = this._createComponent($content, ToolbarMenuList, { - dataSource: this._getListDataSource(), - pageLoadMode: 'scrollBottom', - indicateLoading: false, - noDataText: '', - itemTemplate: this.option('itemTemplate'), - onItemClick: (e) => { - if(this.option('closeOnClick')) { - this.option('opened', false); - } - this._itemClickAction(e); - }, - tabIndex: -1, - focusStateEnabled: false, - activeStateEnabled: true, - onItemRendered: this.option('onItemRendered'), - _itemAttributes: { role: 'menuitem' }, - }); - } + this._popup?.toggle(state); + this.setAria('expanded', state); + } - _itemOptionChanged(item, property, value) { - this._list?._itemOptionChanged(item, property, value); - toggleItemFocusableElementTabIndex(this._list, item); + _renderPopup(): void { + if (this._$popup) { + return; } - _getListDataSource() { - return this.option('dataSource') ?? this.option('items'); - } - - _setListDataSource() { - this._list?.option('dataSource', this._getListDataSource()); - - delete this._deferRendering; - } - - _getKeyboardListeners() { - return super._getKeyboardListeners().concat([this._list]); - } - - _toggleVisibility(visible) { - super._toggleVisibility(visible); - this._button.option('visible', visible); - } + this._$popup = $('
').appendTo(this.$element()); + const { rtlEnabled, container, animation } = this.option(); + + this._popup = this._createComponent(this._$popup, 'dxPopup', { + onInitialized({ component }) { + // @ts-expect-error + component.$wrapper() + .addClass(DROP_DOWN_MENU_POPUP_WRAPPER_CLASS) + .addClass(DROP_DOWN_MENU_POPUP_CLASS); + }, + deferRendering: false, + contentTemplate: (contentElement) => this._renderList(contentElement), + _ignoreFunctionValueDeprecation: true, + maxHeight: () => this._getMaxHeight(), + position: { + // @ts-expect-error + my: `top ${rtlEnabled ? 'left' : 'right'}`, + // @ts-expect-error + at: `bottom ${rtlEnabled ? 'left' : 'right'}`, + collision: 'fit flip', + // @ts-expect-error + offset: { v: POPUP_VERTICAL_OFFSET }, + of: this.$element(), + }, + animation, + onOptionChanged: ({ name, value }) => { + if (name === 'visible') { + this.option('opened', value); + } + }, + container, + autoResizeEnabled: false, + height: 'auto', + width: 'auto', + hideOnOutsideClick: (e) => this._closeOutsideDropDownHandler(e), + hideOnParentScroll: true, + shading: false, + dragEnabled: false, + showTitle: false, + fullScreen: false, + _fixWrapperPosition: true, + }); + } + + _getMaxHeight() { + const $element = this.$element(); + + // @ts-expect-error + const offsetTop = $element.offset().top; + const windowHeight = getOuterHeight(getWindow()); + const maxHeight = Math.max(offsetTop, windowHeight - offsetTop - getOuterHeight($element)); + + return Math.min(windowHeight, maxHeight - POPUP_VERTICAL_OFFSET - POPUP_BOUNDARY_VERTICAL_OFFSET); + } + + _closeOutsideDropDownHandler(e): boolean { + // @ts-expect-error + const isOutsideClick = !$(e.target).closest(this.$element()).length; + + return isOutsideClick; + } + + _renderList(contentElement): void { + const $content = $(contentElement); + $content.addClass(DROP_DOWN_MENU_LIST_CLASS); + + // @ts-expect-error + this._list = this._createComponent($content, ToolbarMenuList, { + dataSource: this._getListDataSource(), + pageLoadMode: 'scrollBottom', + indicateLoading: false, + noDataText: '', + itemTemplate: this.option('itemTemplate'), + onItemClick: (e) => { + if (this.option('closeOnClick')) { + this.option('opened', false); + } + this._itemClickAction(e); + }, + tabIndex: -1, + focusStateEnabled: false, + activeStateEnabled: true, + onItemRendered: this.option('onItemRendered'), + _itemAttributes: { role: 'menuitem' }, + }); + } + + _itemOptionChanged(item, property, value): void { + // @ts-expect-error + this._list?._itemOptionChanged(item, property, value); + toggleItemFocusableElementTabIndex(this._list, item); + } + + _getListDataSource(): any { + return this.option('dataSource') ?? this.option('items'); + } + + _setListDataSource(): void { + this._list?.option('dataSource', this._getListDataSource()); + + delete this._deferRendering; + } + + _getKeyboardListeners() { + return super._getKeyboardListeners().concat([this._list]); + } + + _toggleVisibility(visible): void { + super._toggleVisibility(visible); + this._button?.option('visible', visible); + } + + _optionChanged(args) { + const { name, value } = args; + + switch (name) { + case 'items': + case 'dataSource': + if (!this.option('opened')) { + this._deferRendering = true; + } else { + this._setListDataSource(); + } + break; + case 'itemTemplate': + this._list?.option(name, this._getTemplate(value)); + break; + case 'onItemClick': + this._initItemClickAction(); + break; + case 'onButtonClick': + this._buttonClickAction(); + break; + case 'useInkRipple': + this._invalidate(); + break; + case 'focusStateEnabled': + this._list?.option(name, value); + super._optionChanged(args); + break; + case 'onItemRendered': + this._list?.option(name, value); + break; + case 'opened': + if (this._deferRendering) { + this._setListDataSource(); + } - _optionChanged(args) { - const { name, value } = args; - - switch(name) { - case 'items': - case 'dataSource': - if(!this.option('opened')) { - this._deferRendering = true; - } else { - this._setListDataSource(); - } - break; - case 'itemTemplate': - this._list?.option(name, this._getTemplate(value)); - break; - case 'onItemClick': - this._initItemClickAction(); - break; - case 'onButtonClick': - this._buttonClickAction(); - break; - case 'useInkRipple': - this._invalidate(); - break; - case 'focusStateEnabled': - this._list?.option(name, value); - super._optionChanged(args); - break; - case 'onItemRendered': - this._list?.option(name, value); - break; - case 'opened': - if(this._deferRendering) { - this._setListDataSource(); - } - - this._toggleMenuVisibility(value); - this._updateFocusableItemsTabIndex(); - break; - case 'closeOnClick': - break; - case 'container': - this._popup && this._popup.option(name, value); - break; - case 'disabled': - if(this._list) { - this._updateFocusableItemsTabIndex(); - } - break; - default: - super._optionChanged(args); + this._toggleMenuVisibility(value); + this._updateFocusableItemsTabIndex(); + break; + case 'closeOnClick': + break; + case 'container': + this._popup && this._popup.option(name, value); + break; + case 'disabled': + if (this._list) { + this._updateFocusableItemsTabIndex(); } + break; + default: + super._optionChanged(args); } + } - _updateFocusableItemsTabIndex() { - this.option('items').forEach(item => toggleItemFocusableElementTabIndex(this._list, item)); - } + _updateFocusableItemsTabIndex(): void { + // @ts-expect-error + this.option('items').forEach((item) => toggleItemFocusableElementTabIndex(this._list, item)); + } } diff --git a/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.base.ts b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.base.ts index 5eccb4b2a2ff..bf26d23979f5 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.base.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.base.ts @@ -1,16 +1,23 @@ -import { getWidth, getOuterWidth, getHeight } from '../../core/utils/size'; -import $ from '../../core/renderer'; -import { isMaterial, isMaterialBased, waitWebFont } from '../themes'; -import { isPlainObject, isDefined } from '../../core/utils/type'; -import registerComponent from '../../core/component_registrator'; -import { extend } from '../../core/utils/extend'; -import { each } from '../../core/utils/iterator'; -import { getBoundingRect } from '../../core/utils/position'; -import AsyncCollectionWidget from '../collection/ui.collection_widget.async'; -import { BindableTemplate } from '../../core/templates/bindable_template'; -import fx from '../../animation/fx'; - -import { TOOLBAR_CLASS } from './constants'; +import fx from '@js/animation/fx'; +import registerComponent from '@js/core/component_registrator'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { BindableTemplate } from '@js/core/templates/bindable_template'; +import { each } from '@js/core/utils/iterator'; +import { getBoundingRect } from '@js/core/utils/position'; +import { getHeight, getOuterWidth, getWidth } from '@js/core/utils/size'; +import { isDefined, isPlainObject } from '@js/core/utils/type'; +import { + isMaterial, + isMaterialBased, + // @ts-expect-error + waitWebFont, +} from '@js/ui/themes'; +import type { Item, Properties as PublicProperties } from '@js/ui/toolbar'; +import AsyncCollectionWidget from '@ts/ui/collection/async'; + +import type { TypedCollectionWidgetOptions } from '../collection/base'; +import { TOOLBAR_CLASS } from './m_constants'; const TOOLBAR_BEFORE_CLASS = 'dx-toolbar-before'; const TOOLBAR_CENTER_CLASS = 'dx-toolbar-center'; @@ -30,440 +37,490 @@ const DEFAULT_DROPDOWNBUTTON_STYLING_MODE = 'contained'; const TOOLBAR_ITEM_DATA_KEY = 'dxToolbarItemDataKey'; const ANIMATION_TIMEOUT = 15; -class ToolbarBase extends AsyncCollectionWidget { - _getSynchronizableOptionsForCreateComponent() { - return super._getSynchronizableOptionsForCreateComponent().filter(item => item !== 'disabled'); - } - - _initTemplates() { - super._initTemplates(); - const template = new BindableTemplate(function($container, data, rawModel) { - if(isPlainObject(data)) { - const { text, html, widget } = data; - - if(text) { - $container.text(text).wrapInner('
'); - } - - if(html) { - $container.html(html); - } - - if(widget === 'dxDropDownButton') { - data.options = data.options ?? {}; - - if(!isDefined(data.options.stylingMode)) { - data.options.stylingMode = this.option('useFlatButtons') - ? TEXT_BUTTON_MODE - : DEFAULT_DROPDOWNBUTTON_STYLING_MODE; - } - } - - if(widget === 'dxButton') { - if(this.option('useFlatButtons')) { - data.options = data.options ?? {}; - data.options.stylingMode = data.options.stylingMode ?? TEXT_BUTTON_MODE; - } - - if(this.option('useDefaultButtons')) { - data.options = data.options ?? {}; - data.options.type = data.options.type ?? DEFAULT_BUTTON_TYPE; - } - } - } else { - $container.text(String(data)); - } - - this._getTemplate('dx-polymorph-widget').render({ - container: $container, - model: rawModel, - parent: this - }); - }.bind(this), ['text', 'html', 'widget', 'options'], this.option('integrationOptions.watchMethod')); - - this._templateManager.addDefaultTemplates({ - item: template, - menuItem: template, - }); - } - - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - renderAs: 'topToolbar', - grouped: false, - useFlatButtons: false, - useDefaultButtons: false, - }); - } - - _defaultOptionsRules() { - return super._defaultOptionsRules().concat([ - { - device: function() { - return isMaterialBased(); - }, - options: { - useFlatButtons: true - } - } - ]); - } - - _itemContainer() { - return this._$toolbarItemsContainer.find([ - `.${TOOLBAR_BEFORE_CLASS}`, - `.${TOOLBAR_CENTER_CLASS}`, - `.${TOOLBAR_AFTER_CLASS}`, - ].join(',')); - } - - _itemClass() { - return TOOLBAR_ITEM_CLASS; - } - - _itemDataKey() { - return TOOLBAR_ITEM_DATA_KEY; - } - - _dimensionChanged() { - if(this._disposed) { - return; - } - - this._arrangeItems(); - this._applyCompactMode(); - } - - _initMarkup() { - this._renderToolbar(); - this._renderSections(); - - super._initMarkup(); - } - - _render() { - super._render(); - this._renderItemsAsync(); - - this._updateDimensionsInMaterial(); - } - - _postProcessRenderItems() { - this._arrangeItems(); - } - - _renderToolbar() { - this.$element() - .addClass(TOOLBAR_CLASS); +type ItemLike = string | Item | any; + +export interface Properties< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends ItemLike = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, +> extends PublicProperties, + Omit< + TypedCollectionWidgetOptions, + keyof PublicProperties & keyof TypedCollectionWidgetOptions + > { +} - this._$toolbarItemsContainer = $('
') - .addClass(TOOLBAR_ITEMS_CONTAINER_CLASS) - .appendTo(this.$element()); +class ToolbarBase extends AsyncCollectionWidget { + _$toolbarItemsContainer?: any; - this.setAria('role', 'toolbar'); - } + _$beforeSection?: dxElementWrapper; - _renderSections() { - const $container = this._$toolbarItemsContainer; + _$centerSection?: dxElementWrapper; - each(['before', 'center', 'after'], (_, section) => { - const sectionClass = `dx-toolbar-${section}`; - const $section = $container.find(`.${sectionClass}`); + _$afterSection?: dxElementWrapper; - if(!$section.length) { - this[`_$${section}Section`] = $('
') - .addClass(sectionClass) - .attr('role', 'presentation') - .appendTo($container); - } - }); - } + _waitParentAnimationTimeout?: any; - _arrangeItems(elementWidth) { - elementWidth = elementWidth ?? getWidth(this.$element()); + _getSynchronizableOptionsForCreateComponent(): string[] { + return super._getSynchronizableOptionsForCreateComponent().filter((item) => item !== 'disabled'); + } - this._$centerSection.css({ - margin: '0 auto', - float: 'none' - }); + _initTemplates(): void { + super._initTemplates(); - const beforeRect = getBoundingRect(this._$beforeSection.get(0)); - const afterRect = getBoundingRect(this._$afterSection.get(0)); + const template = new BindableTemplate(($container, data, rawModel) => { + if (isPlainObject(data)) { + const { text, html, widget } = data; - this._alignCenterSection(beforeRect, afterRect, elementWidth); - - const $label = this._$toolbarItemsContainer.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0); - const $section = $label.parent(); - - if(!$label.length) { - return; + if (text) { + $container.text(text).wrapInner('
'); } - const labelOffset = beforeRect.width ? beforeRect.width : $label.position().left; - const widthBeforeSection = $section.hasClass(TOOLBAR_BEFORE_CLASS) ? 0 : labelOffset; - const widthAfterSection = $section.hasClass(TOOLBAR_AFTER_CLASS) ? 0 : afterRect.width; - let elemsAtSectionWidth = 0; - - $section.children().not(`.${TOOLBAR_LABEL_CLASS}`).each(function() { - elemsAtSectionWidth += getOuterWidth(this); - }); - - const freeSpace = elementWidth - elemsAtSectionWidth; - const sectionMaxWidth = Math.max(freeSpace - widthBeforeSection - widthAfterSection, 0); - - if($section.hasClass(TOOLBAR_BEFORE_CLASS)) { - this._alignSection(this._$beforeSection, sectionMaxWidth); - } else { - const labelPaddings = getOuterWidth($label) - getWidth($label); - $label.css('maxWidth', sectionMaxWidth - labelPaddings); + if (html) { + $container.html(html); } - } - - _alignCenterSection(beforeRect, afterRect, elementWidth) { - this._alignSection(this._$centerSection, elementWidth - beforeRect.width - afterRect.width); - const isRTL = this.option('rtlEnabled'); - const leftRect = isRTL ? afterRect : beforeRect; - const rightRect = isRTL ? beforeRect : afterRect; - const centerRect = getBoundingRect(this._$centerSection.get(0)); + if (widget === 'dxDropDownButton') { + data.options = data.options ?? {}; - if(leftRect.right > centerRect.left || centerRect.right > rightRect.left) { - this._$centerSection.css({ - marginLeft: leftRect.width, - marginRight: rightRect.width, - float: leftRect.width > rightRect.width ? 'none' : 'right' - }); + if (!isDefined(data.options.stylingMode)) { + data.options.stylingMode = this.option('useFlatButtons') + ? TEXT_BUTTON_MODE + : DEFAULT_DROPDOWNBUTTON_STYLING_MODE; + } } - } - - _alignSection($section, maxWidth) { - const $labels = $section.find(`.${TOOLBAR_LABEL_CLASS}`); - let labels = $labels.toArray(); - - maxWidth = maxWidth - this._getCurrentLabelsPaddings(labels); - const currentWidth = this._getCurrentLabelsWidth(labels); - const difference = Math.abs(currentWidth - maxWidth); + if (widget === 'dxButton') { + if (this.option('useFlatButtons')) { + data.options = data.options ?? {}; + data.options.stylingMode = data.options.stylingMode ?? TEXT_BUTTON_MODE; + } - if(maxWidth < currentWidth) { - labels = labels.reverse(); - this._alignSectionLabels(labels, difference, false); - } else { - this._alignSectionLabels(labels, difference, true); + if (this.option('useDefaultButtons')) { + data.options = data.options ?? {}; + data.options.type = data.options.type ?? DEFAULT_BUTTON_TYPE; + } } + } else { + $container.text(String(data)); + } + + this._getTemplate('dx-polymorph-widget').render({ + container: $container, + model: rawModel, + // @ts-expect-error + parent: this, + }); + }, ['text', 'html', 'widget', 'options'], this.option('integrationOptions.watchMethod')); + + this._templateManager.addDefaultTemplates({ + item: template, + menuItem: template, + }); + } + + _getDefaultOptions(): Properties { + return { + ...super._getDefaultOptions(), + // @ts-expect-error + renderAs: 'topToolbar', + grouped: false, + useFlatButtons: false, + useDefaultButtons: false, + }; + } + + _defaultOptionsRules() { + return super._defaultOptionsRules().concat([ + { + device() { + // @ts-expect-error + return isMaterialBased(); + }, + options: { + useFlatButtons: true, + }, + }, + ]); + } + + _itemContainer() { + return this._$toolbarItemsContainer.find([ + `.${TOOLBAR_BEFORE_CLASS}`, + `.${TOOLBAR_CENTER_CLASS}`, + `.${TOOLBAR_AFTER_CLASS}`, + ].join(',')); + } + + _itemClass(): string { + return TOOLBAR_ITEM_CLASS; + } + + _itemDataKey(): string { + return TOOLBAR_ITEM_DATA_KEY; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _dimensionChanged(dimension?: 'height' | 'width'): void { + if (this._disposed) { + return; + } + + this._arrangeItems(); + this._applyCompactMode(); + } + + _initMarkup(): void { + this._renderToolbar(); + this._renderSections(); + + super._initMarkup(); + } + + _render(): void { + super._render(); + this._renderItemsAsync(); + + this._updateDimensionsInMaterial(); + } + + _postProcessRenderItems() { + this._arrangeItems(); + } + + _renderToolbar(): void { + this.$element() + // @ts-expect-error + .addClass(TOOLBAR_CLASS); + + this._$toolbarItemsContainer = $('
') + .addClass(TOOLBAR_ITEMS_CONTAINER_CLASS) + .appendTo(this.$element()); + + this.setAria('role', 'toolbar'); + } + + _renderSections(): void { + const $container = this._$toolbarItemsContainer; + + each(['before', 'center', 'after'], (_, section) => { + const sectionClass = `dx-toolbar-${section}`; + const $section = $container.find(`.${sectionClass}`); + + if (!$section.length) { + this[`_$${section}Section`] = $('
') + .addClass(sectionClass) + .attr('role', 'presentation') + .appendTo($container); + } + }); + } + + _arrangeItems(width?: number) { + const elementWidth = width ?? getWidth(this.$element()); + // @ts-expect-error + this._$centerSection.css({ + margin: '0 auto', + float: 'none', + }); + + const beforeRect = getBoundingRect(this._$beforeSection?.get(0)); + const afterRect = getBoundingRect(this._$afterSection?.get(0)); + + this._alignCenterSection(beforeRect, afterRect, elementWidth); + + const $label = this._$toolbarItemsContainer.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0); + const $section: dxElementWrapper = $label.parent(); + + if (!$label.length) { + return; } - _alignSectionLabels(labels, difference, expanding) { - const getRealLabelWidth = function(label) { return getBoundingRect(label).width; }; - - for(let i = 0; i < labels.length; i++) { - const $label = $(labels[i]); - const currentLabelWidth = Math.ceil(getRealLabelWidth(labels[i])); - let labelMaxWidth; - - if(expanding) { - $label.css('maxWidth', 'inherit'); - } - - const possibleLabelWidth = Math.ceil(expanding ? getRealLabelWidth(labels[i]) : currentLabelWidth); - - if(possibleLabelWidth < difference) { - labelMaxWidth = expanding ? possibleLabelWidth : 0; - difference = difference - possibleLabelWidth; - } else { - labelMaxWidth = expanding ? currentLabelWidth + difference : currentLabelWidth - difference; - $label.css('maxWidth', labelMaxWidth); - break; - } + const labelOffset = beforeRect.width ? beforeRect.width : $label.position().left; + const widthBeforeSection = $section.hasClass(TOOLBAR_BEFORE_CLASS) ? 0 : labelOffset; + const widthAfterSection = $section.hasClass(TOOLBAR_AFTER_CLASS) ? 0 : afterRect.width; + let elemsAtSectionWidth = 0; - $label.css('maxWidth', labelMaxWidth); - } - } + // @ts-expect-error + $section.children().not(`.${TOOLBAR_LABEL_CLASS}`).each((index, element) => { + elemsAtSectionWidth += getOuterWidth(element); + }); - _applyCompactMode() { - const $element = this.$element(); - $element.removeClass(TOOLBAR_COMPACT_CLASS); + const freeSpace = elementWidth - elemsAtSectionWidth; + const sectionMaxWidth = Math.max(freeSpace - widthBeforeSection - widthAfterSection, 0); - if(this.option('compactMode') && this._getSummaryItemsSize('width', this.itemElements(), true) > getWidth($element)) { - $element.addClass(TOOLBAR_COMPACT_CLASS); - } + if ($section.hasClass(TOOLBAR_BEFORE_CLASS)) { + this._alignSection(this._$beforeSection, sectionMaxWidth); + } else { + const labelPaddings = getOuterWidth($label) - getWidth($label); + $label.css('maxWidth', sectionMaxWidth - labelPaddings); } + } - _getCurrentLabelsWidth(labels) { - let width = 0; + _alignCenterSection(beforeRect, afterRect, elementWidth): void { + this._alignSection(this._$centerSection, elementWidth - beforeRect.width - afterRect.width); - labels.forEach(function(label, index) { - width += getOuterWidth(label); - }); + const isRTL = this.option('rtlEnabled'); + const leftRect = isRTL ? afterRect : beforeRect; + const rightRect = isRTL ? beforeRect : afterRect; + const centerRect = getBoundingRect(this._$centerSection?.get(0)); - return width; + if (leftRect.right > centerRect.left || centerRect.right > rightRect.left) { + // @ts-expect-error + this._$centerSection.css({ + marginLeft: leftRect.width, + marginRight: rightRect.width, + float: leftRect.width > rightRect.width ? 'none' : 'right', + }); } + } - _getCurrentLabelsPaddings(labels) { - let padding = 0; + _alignSection($section, maxWidth): void { + const $labels = $section.find(`.${TOOLBAR_LABEL_CLASS}`); + let labels = $labels.toArray(); - labels.forEach(function(label, index) { - padding += (getOuterWidth(label) - getWidth(label)); - }); + maxWidth -= this._getCurrentLabelsPaddings(labels); - return padding; - } - - _renderItem(index, item, itemContainer, $after) { - const location = item.location ?? 'center'; - const container = itemContainer ?? this[`_$${location}Section`]; - const itemHasText = !!(item.text ?? item.html); - const itemElement = super._renderItem(index, item, container, $after); - - itemElement - .toggleClass(TOOLBAR_BUTTON_CLASS, !itemHasText) - .toggleClass(TOOLBAR_LABEL_CLASS, itemHasText) - .addClass(item.cssClass); + const currentWidth = this._getCurrentLabelsWidth(labels); + const difference = Math.abs(currentWidth - maxWidth); - return itemElement; + if (maxWidth < currentWidth) { + labels = labels.reverse(); + this._alignSectionLabels(labels, difference, false); + } else { + this._alignSectionLabels(labels, difference, true); } + } - _renderGroupedItems() { - each(this.option('items'), (groupIndex, group) => { - const groupItems = group.items; - const $container = $('
').addClass(TOOLBAR_GROUP_CLASS); - const location = group.location ?? 'center'; + _alignSectionLabels(labels, difference, expanding): void { + const getRealLabelWidth = function (label) { return getBoundingRect(label).width; }; - if(!groupItems || !groupItems.length) { - return; - } + for (let i = 0; i < labels.length; i++) { + const $label = $(labels[i]); + const currentLabelWidth = Math.ceil(getRealLabelWidth(labels[i])); + let labelMaxWidth; - each(groupItems, (itemIndex, item) => { - this._renderItem(itemIndex, item, $container, null); - }); + if (expanding) { + $label.css('maxWidth', 'inherit'); + } - this._$toolbarItemsContainer.find(`.dx-toolbar-${location}`).append($container); - }); - } + const possibleLabelWidth = Math.ceil(expanding ? getRealLabelWidth(labels[i]) : currentLabelWidth); - _renderItems(items) { - const grouped = this.option('grouped') && items.length && items[0].items; - grouped ? this._renderGroupedItems() : super._renderItems(items); - } + if (possibleLabelWidth < difference) { + labelMaxWidth = expanding ? possibleLabelWidth : 0; + difference -= possibleLabelWidth; + } else { + labelMaxWidth = expanding ? currentLabelWidth + difference : currentLabelWidth - difference; + $label.css('maxWidth', labelMaxWidth); + break; + } - _getToolbarItems() { - return this.option('items') ?? []; + $label.css('maxWidth', labelMaxWidth); } + } - _renderContentImpl() { - const items = this._getToolbarItems(); - - this.$element().toggleClass(TOOLBAR_MINI_CLASS, items.length === 0); + _applyCompactMode(): void { + const $element = $(this.element()); + $element.removeClass(TOOLBAR_COMPACT_CLASS); - if(this._renderedItemsCount) { - this._renderItems(items.slice(this._renderedItemsCount)); - } else { - this._renderItems(items); - } - - this._applyCompactMode(); + if (this.option('compactMode') && this._getSummaryItemsSize('width', this._itemElements(), true) > getWidth($element)) { + $element.addClass(TOOLBAR_COMPACT_CLASS); } + } - _renderEmptyMessage() {} + _getCurrentLabelsWidth(labels) { + let width = 0; - _clean() { - this._$toolbarItemsContainer.children().empty(); - this.$element().empty(); - - delete this._$beforeSection; - delete this._$centerSection; - delete this._$afterSection; - } - - _visibilityChanged(visible) { - if(visible) { - this._arrangeItems(); - } - } + labels.forEach((label) => { + width += getOuterWidth(label); + }); - _isVisible() { - return getWidth(this.$element()) > 0 && getHeight(this.$element()) > 0; - } + return width; + } - _getIndexByItem(item) { - return this._getToolbarItems().indexOf(item); - } + _getCurrentLabelsPaddings(labels) { + let padding = 0; - _itemOptionChanged(item, property, value) { - super._itemOptionChanged.apply(this, [item, property, value]); - this._arrangeItems(); - } + labels.forEach((label) => { + padding += getOuterWidth(label) - getWidth(label); + }); - _optionChanged({ name, value }) { - switch(name) { - case 'width': - super._optionChanged.apply(this, arguments); - this._dimensionChanged(); - break; - case 'renderAs': - case 'useFlatButtons': - case 'useDefaultButtons': - this._invalidate(); - break; - case 'compactMode': - this._applyCompactMode(); - break; - case 'grouped': - break; - default: - super._optionChanged.apply(this, arguments); - } - } + return padding; + } - _dispose() { - super._dispose(); - clearTimeout(this._waitParentAnimationTimeout); - } + _renderItem(index, item, itemContainer, $after) { + const location = item.location ?? 'center'; + const container = itemContainer ?? this[`_$${location}Section`]; + const itemHasText = !!(item.text ?? item.html); + const itemElement = super._renderItem(index, item, container, $after); + + itemElement + .toggleClass(TOOLBAR_BUTTON_CLASS, !itemHasText) + .toggleClass(TOOLBAR_LABEL_CLASS, itemHasText) + .addClass(item.cssClass); + + return itemElement; + } + + _renderGroupedItems(): void { + each(this.option('items'), (groupIndex, group) => { + // @ts-expect-error + const groupItems = group.items; + const $container = $('
').addClass(TOOLBAR_GROUP_CLASS); + // @ts-expect-error + const location = group.location ?? 'center'; + + if (!groupItems || !groupItems.length) { + return; + } + + each(groupItems, (itemIndex, item) => { + this._renderItem(itemIndex, item, $container, null); + }); + + this._$toolbarItemsContainer.find(`.dx-toolbar-${location}`).append($container); + }); + } + + _renderItems(items) { + const grouped = this.option('grouped') && items.length && items[0].items; + grouped ? this._renderGroupedItems() : super._renderItems(items); + } + + _getToolbarItems(): Item[] { + return this.option('items') ?? []; + } + + _renderContentImpl(): void { + const items = this._getToolbarItems(); + // @ts-expect-error + this.$element().toggleClass(TOOLBAR_MINI_CLASS, items.length === 0); + + if (this._renderedItemsCount) { + this._renderItems(items.slice(this._renderedItemsCount)); + } else { + this._renderItems(items); + } + + this._applyCompactMode(); + } + + _renderEmptyMessage(): void {} + + _clean(): void { + this._$toolbarItemsContainer.children().empty(); + // @ts-expect-error + this.$element().empty(); + + delete this._$beforeSection; + delete this._$centerSection; + delete this._$afterSection; + } + + _visibilityChanged(visible): void { + if (visible) { + this._arrangeItems(); + } + } + + _isVisible(): boolean { + return getWidth(this.$element()) > 0 && getHeight(this.$element()) > 0; + } + + _getIndexByItem(item): number { + return this._getToolbarItems().indexOf(item); + } + + _itemOptionChanged( + item: Item, + property: string, + value: unknown, + prevValue: unknown, + ): void { + super._itemOptionChanged(item, property, value, prevValue); + this._arrangeItems(); + } + + _optionChanged(args: Record): void { + const { name } = args; + + switch (name) { + case 'width': + super._optionChanged(args); + this._dimensionChanged(); + break; + case 'renderAs': + case 'useFlatButtons': + case 'useDefaultButtons': + this._invalidate(); + break; + case 'compactMode': + this._applyCompactMode(); + break; + case 'grouped': + break; + default: + super._optionChanged(args); + } + } + + _dispose(): void { + super._dispose(); + clearTimeout(this._waitParentAnimationTimeout); + } + + _updateDimensionsInMaterial() { + // @ts-expect-error + if (isMaterial()) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const _waitParentAnimationFinished = () => new Promise((resolve) => { + const check = () => { + let readyToResolve = true; + // @ts-expect-error + this.$element().parents().each((_, parent) => { + // @ts-expect-error + if (fx.isAnimating($(parent))) { + readyToResolve = false; + return false; + } + }); + if (readyToResolve) { + // @ts-expect-error + resolve(); + } + return readyToResolve; + }; + const runCheck = () => { + clearTimeout(this._waitParentAnimationTimeout); + this._waitParentAnimationTimeout = setTimeout(() => check() || runCheck(), ANIMATION_TIMEOUT); + }; + runCheck(); + }); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const _checkWebFontForLabelsLoaded = () => { + // @ts-expect-error + const $labels = this.$element().find(`.${TOOLBAR_LABEL_CLASS}`); + const promises = []; + $labels.each((_, label) => { + const text = $(label).text(); + // @ts-expect-error + const fontWeight = $(label).css('fontWeight'); + // @ts-expect-error + promises.push(waitWebFont(text, fontWeight)); + }); + return Promise.all(promises); + }; - _updateDimensionsInMaterial() { - if(isMaterial()) { - const _waitParentAnimationFinished = () => { - return new Promise(resolve => { - const check = () => { - let readyToResolve = true; - this.$element().parents().each((_, parent) => { - if(fx.isAnimating($(parent))) { - readyToResolve = false; - return false; - } - }); - if(readyToResolve) { - resolve(); - } - return readyToResolve; - }; - const runCheck = () => { - clearTimeout(this._waitParentAnimationTimeout); - this._waitParentAnimationTimeout = setTimeout(() => check() || runCheck(), ANIMATION_TIMEOUT); - }; - runCheck(); - }); - }; - - const _checkWebFontForLabelsLoaded = () => { - const $labels = this.$element().find(`.${TOOLBAR_LABEL_CLASS}`); - const promises = []; - $labels.each((_, label) => { - const text = $(label).text(); - const fontWeight = $(label).css('fontWeight'); - promises.push(waitWebFont(text, fontWeight)); - }); - return Promise.all(promises); - }; - - Promise.all([ - _waitParentAnimationFinished(), - _checkWebFontForLabelsLoaded(), - ]).then(() => { this._dimensionChanged(); }); - } + Promise.all([ + _waitParentAnimationFinished(), + _checkWebFontForLabelsLoaded(), + ]).then(() => { this._dimensionChanged(); }); } + } } - +// @ts-expect-error registerComponent('dxToolbarBase', ToolbarBase); export default ToolbarBase; diff --git a/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.ts b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.ts index 2a9e4986f559..c805a92193a6 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.ts @@ -1,255 +1,192 @@ -import registerComponent from '../../core/component_registrator'; -import { extend } from '../../core/utils/extend'; -import ToolbarBase from './ui.toolbar.base'; -import { toggleItemFocusableElementTabIndex } from './ui.toolbar.utils'; -import { MultiLineStrategy } from './strategy/toolbar.multiline'; -import { SingleLineStrategy } from './strategy/toolbar.singleline'; +import registerComponent from '@js/core/component_registrator'; +import type { dxElementWrapper } from '@js/core/renderer'; -// STYLE toolbar +import type { Properties as ToolbarBaseProperties } from './m_toolbar.base'; +import ToolbarBase from './m_toolbar.base'; +import { toggleItemFocusableElementTabIndex } from './m_toolbar.utils'; +import { MultiLineStrategy } from './strategy/m_toolbar.multiline'; +import { SingleLineStrategy } from './strategy/m_toolbar.singleline'; const TOOLBAR_MULTILINE_CLASS = 'dx-toolbar-multiline'; const TOOLBAR_AUTO_HIDE_TEXT_CLASS = 'dx-toolbar-text-auto-hide'; +export interface Properties extends ToolbarBaseProperties { + menuContainer?: dxElementWrapper; + overflowMenuVisible?: boolean; +} class Toolbar extends ToolbarBase { + _layoutStrategy!: MultiLineStrategy | SingleLineStrategy; - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - - menuItemTemplate: 'menuItem', - menuContainer: undefined, - overflowMenuVisible: false, - multiline: false, - - /** - * @name dxToolbarOptions.selectedIndex - * @type number - * @default -1 - * @hidden - */ - - /** - * @name dxToolbarOptions.activeStateEnabled - * @hidden - */ - - /** - * @name dxToolbarOptions.focusStateEnabled - * @hidden - */ - - /** - * @name dxToolbarOptions.accessKey - * @hidden - */ - - /** - * @name dxToolbarOptions.tabIndex - * @hidden - */ - - /** - * @name dxToolbarOptions.selectedItems - * @hidden - */ - - /** - * @name dxToolbarOptions.selectedItemKeys - * @hidden - */ - - /** - * @name dxToolbarOptions.keyExpr - * @hidden - */ - - /** - * @name dxToolbarOptions.selectedItem - * @hidden - */ - - /** - * @name dxToolbarOptions.height - * @hidden - */ - - /** - * @name dxToolbarOptions.onSelectionChanged - * @action - * @hidden - */ - }); + _getDefaultOptions(): Properties { + return { + ...super._getDefaultOptions(), + menuItemTemplate: 'menuItem', + menuContainer: undefined, + overflowMenuVisible: false, + multiline: false, + }; + } - } + _isMultiline() { + return this.option('multiline'); + } - _isMultiline() { - return this.option('multiline'); + _dimensionChanged(dimension?: 'height' | 'width'): void { + if (dimension === 'height') { + return; } - _dimensionChanged(dimension) { - if(dimension === 'height') { - return; - } + super._dimensionChanged(); - super._dimensionChanged(); + this._layoutStrategy._dimensionChanged(); + } - this._layoutStrategy._dimensionChanged(); - } + _initMarkup(): void { + super._initMarkup(); - _initMarkup() { - super._initMarkup(); + this._updateFocusableItemsTabIndex(); - this._updateFocusableItemsTabIndex(); + this._layoutStrategy._initMarkup(); + } - this._layoutStrategy._initMarkup(); - } + _renderToolbar(): void { + super._renderToolbar(); - _renderToolbar() { - super._renderToolbar(); + this._renderLayoutStrategy(); + } - this._renderLayoutStrategy(); + _itemContainer() { + if (this._isMultiline()) { + return this._$toolbarItemsContainer; } - _itemContainer() { - if(this._isMultiline()) { - return this._$toolbarItemsContainer; - } + return super._itemContainer(); + } - return super._itemContainer(); + _renderLayoutStrategy(): void { + // @ts-expect-error + this.$element().toggleClass(TOOLBAR_MULTILINE_CLASS, this._isMultiline()); + + this._layoutStrategy = this._isMultiline() + ? new MultiLineStrategy(this) + : new SingleLineStrategy(this); + } + + _renderSections() { + if (this._isMultiline()) { + return; } - _renderLayoutStrategy() { + return super._renderSections(); + } - this.$element().toggleClass(TOOLBAR_MULTILINE_CLASS, this._isMultiline()); + _postProcessRenderItems(): void { + this._layoutStrategy._hideOverflowItems(); + this._layoutStrategy._updateMenuVisibility(); - this._layoutStrategy = this._isMultiline() - ? new MultiLineStrategy(this) - : new SingleLineStrategy(this); - } + super._postProcessRenderItems(); - _renderSections() { - if(this._isMultiline()) { - return; - } + this._layoutStrategy._renderMenuItems(); + } - return super._renderSections(); - } + _renderItem(index, item, itemContainer, $after) { + const itemElement = super._renderItem(index, item, itemContainer, $after); - _postProcessRenderItems() { - this._layoutStrategy._hideOverflowItems(); - this._layoutStrategy._updateMenuVisibility(); + this._layoutStrategy._renderItem(item, itemElement); - super._postProcessRenderItems(); + const { widget, showText } = item; - this._layoutStrategy._renderMenuItems(); + if (widget === 'dxButton' && showText === 'inMenu') { + itemElement.toggleClass(TOOLBAR_AUTO_HIDE_TEXT_CLASS); } - _renderItem(index, item, itemContainer, $after) { - const itemElement = super._renderItem(index, item, itemContainer, $after); + return itemElement; + } - this._layoutStrategy._renderItem(item, itemElement); + // for filemanager + _getItemsWidth() { + return this._layoutStrategy._getItemsWidth(); + } - const { widget, showText } = item; + // for filemanager + _getMenuItems() { + return this._layoutStrategy._getMenuItems(); + } - if(widget === 'dxButton' && showText === 'inMenu') { - itemElement.toggleClass(TOOLBAR_AUTO_HIDE_TEXT_CLASS); - } + _getToolbarItems() { + return this._layoutStrategy._getToolbarItems(); + } - return itemElement; + _arrangeItems(): void { + // @ts-expect-error + if (this.$element().is(':hidden')) { + return; } - // for filemanager - _getItemsWidth() { - return this._layoutStrategy._getItemsWidth(); - } + const elementWidth = this._layoutStrategy._arrangeItems(); - // for filemanager - _getMenuItems() { - return this._layoutStrategy._getMenuItems(); + if (!this._isMultiline()) { + super._arrangeItems(elementWidth as number); } + } - _getToolbarItems() { - return this._layoutStrategy._getToolbarItems(); + _itemOptionChanged(item, property, value, prevValue): void { + if (!this._isMenuItem(item)) { + super._itemOptionChanged(item, property, value, prevValue); } - _arrangeItems() { - if(this.$element().is(':hidden')) { - return; - } + this._layoutStrategy._itemOptionChanged(item, property, value); - const elementWidth = this._layoutStrategy._arrangeItems(); + if (property === 'disabled' || property === 'options.disabled') { + toggleItemFocusableElementTabIndex(this, item); + } - if(!this._isMultiline()) { - super._arrangeItems(elementWidth); - } + if (property === 'location') { + this.repaint(); } + } - _itemOptionChanged(item, property, value) { - if(!this._isMenuItem(item)) { - super._itemOptionChanged(item, property, value); - } + _updateFocusableItemsTabIndex(): void { + this._getToolbarItems().forEach((item) => toggleItemFocusableElementTabIndex(this, item)); + } - this._layoutStrategy._itemOptionChanged(item, property, value); + _isMenuItem(itemData): boolean { + return itemData.location === 'menu' || itemData.locateInMenu === 'always'; + } - if(property === 'disabled' || property === 'options.disabled') { - toggleItemFocusableElementTabIndex(this, item); - } + _isToolbarItem(itemData): boolean { + return itemData.location === undefined || itemData.locateInMenu === 'never'; + } - if(property === 'location') { - this.repaint(); - } - } + _optionChanged(args: Record): void { + const { name, value } = args; - _updateFocusableItemsTabIndex() { - this._getToolbarItems().forEach(item => toggleItemFocusableElementTabIndex(this, item)); - } + this._layoutStrategy._optionChanged(name, value); - _isMenuItem(itemData) { - return itemData.location === 'menu' || itemData.locateInMenu === 'always'; - } + switch (name) { + case 'menuContainer': + case 'menuItemTemplate': + case 'overflowMenuVisible': + break; + case 'multiline': + this._invalidate(); + break; + case 'disabled': + super._optionChanged(args); - _isToolbarItem(itemData) { - return itemData.location === undefined || itemData.locateInMenu === 'never'; - } - - _optionChanged({ name, value }) { - this._layoutStrategy._optionChanged(name, value); - - switch(name) { - case 'menuContainer': - case 'menuItemTemplate': - case 'overflowMenuVisible': - break; - case 'multiline': - this._invalidate(); - break; - case 'disabled': - super._optionChanged.apply(this, arguments); - - this._updateFocusableItemsTabIndex(); - break; - default: - super._optionChanged.apply(this, arguments); - } + this._updateFocusableItemsTabIndex(); + break; + default: + super._optionChanged(args); } + } - /** - * @name dxToolbar.registerKeyHandler - * @publicName registerKeyHandler(key, handler) - * @hidden - */ - - /** - * @name dxToolbar.focus - * @publicName focus() - * @hidden - */ - - // it is not public - updateDimensions() { - this._dimensionChanged(); - } + // it is not public + updateDimensions() { + this._dimensionChanged(); + } } - +// @ts-expect-error registerComponent('dxToolbar', Toolbar); export default Toolbar; diff --git a/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.utils.ts b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.utils.ts index b76af90dfc80..9dc5144e816a 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.utils.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/m_toolbar.utils.ts @@ -1,53 +1,52 @@ -import $ from '../../core/renderer'; +import $ from '@js/core/renderer'; const BUTTON_GROUP_CLASS = 'dx-buttongroup'; const TOOLBAR_ITEMS = ['dxAutocomplete', 'dxButton', 'dxCheckBox', 'dxDateBox', 'dxMenu', 'dxSelectBox', 'dxTabs', 'dxTextBox', 'dxButtonGroup', 'dxDropDownButton']; +const getItemInstance = function ($element) { + const itemData = $element.data && $element.data(); + const dxComponents = itemData && itemData.dxComponents; + const widgetName = dxComponents && dxComponents[0]; -const getItemInstance = function($element) { - const itemData = $element.data && $element.data(); - const dxComponents = itemData && itemData.dxComponents; - const widgetName = dxComponents && dxComponents[0]; - - return widgetName && itemData[widgetName]; + return widgetName && itemData[widgetName]; }; export function toggleItemFocusableElementTabIndex(context, item) { - if(!context) return; - - const $item = context._findItemElementByItem(item); - if(!$item.length) { - return; - } + if (!context) return; - const itemData = context._getItemData($item); - const isItemNotFocusable = !!(itemData.options?.disabled || itemData.disabled || context.option('disabled')); + const $item = context._findItemElementByItem(item); + if (!$item.length) { + return; + } - const { widget } = itemData; + const itemData = context._getItemData($item); + const isItemNotFocusable = !!(itemData.options?.disabled || itemData.disabled || context.option('disabled')); - if(widget && TOOLBAR_ITEMS.indexOf(widget) !== -1) { - const $widget = $item.find(widget.toLowerCase().replace('dx', '.dx-')); - if($widget.length) { - const itemInstance = getItemInstance($widget); + const { widget } = itemData; - if(!itemInstance) { - return; - } + if (widget && TOOLBAR_ITEMS.includes(widget)) { + const $widget = $item.find(widget.toLowerCase().replace('dx', '.dx-')); + if ($widget.length) { + const itemInstance = getItemInstance($widget); - let $focusTarget = itemInstance._focusTarget?.(); - - if(widget === 'dxDropDownButton') { - $focusTarget = $focusTarget && $focusTarget.find(`.${BUTTON_GROUP_CLASS}`); - } else { - $focusTarget = $focusTarget ?? $(itemInstance.element()); - } - - const tabIndex = itemData.options?.tabIndex; - if(isItemNotFocusable) { - $focusTarget.attr('tabIndex', -1); - } else { - $focusTarget.attr('tabIndex', tabIndex ?? 0); - } - } + if (!itemInstance) { + return; + } + + let $focusTarget = itemInstance._focusTarget?.(); + + if (widget === 'dxDropDownButton') { + $focusTarget = $focusTarget && $focusTarget.find(`.${BUTTON_GROUP_CLASS}`); + } else { + $focusTarget = $focusTarget ?? $(itemInstance.element()); + } + + const tabIndex = itemData.options?.tabIndex; + if (isItemNotFocusable) { + $focusTarget.attr('tabIndex', -1); + } else { + $focusTarget.attr('tabIndex', tabIndex ?? 0); + } } + } } diff --git a/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.multiline.ts b/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.multiline.ts index 9f0775c7a71a..fec354869c6e 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.multiline.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.multiline.ts @@ -1,46 +1,51 @@ -import { getWidth, getOuterWidth } from '../../../core/utils/size'; +import { getOuterWidth, getWidth } from '@js/core/utils/size'; + +import type Toolbar from '../m_toolbar'; + const TOOLBAR_LABEL_CLASS = 'dx-toolbar-label'; export class MultiLineStrategy { - constructor(toolbar) { - this._toolbar = toolbar; - } + _toolbar: Toolbar; - _initMarkup() {} + constructor(toolbar: Toolbar) { + this._toolbar = toolbar; + } - _updateMenuVisibility() {} + _initMarkup(): void {} - _renderMenuItems() {} + _updateMenuVisibility(): void {} - _renderItem() {} + _renderMenuItems(): void {} - _getMenuItems() {} + _renderItem(): void {} - _getToolbarItems() { - return this._toolbar.option('items') ?? []; - } + _getMenuItems(): void {} - _getItemsWidth() { - return this._toolbar._getSummaryItemsSize('width', this._toolbar.itemElements(), true); - } + _getToolbarItems() { + return this._toolbar.option('items') ?? []; + } - _arrangeItems() { - const $label = this._toolbar._$toolbarItemsContainer.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0); + _getItemsWidth(): number { + return this._toolbar._getSummaryItemsSize('width', this._toolbar._itemElements(), true); + } - if(!$label.length) { - return; - } + _arrangeItems(): void { + const $label = this._toolbar._$toolbarItemsContainer.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0); - const elementWidth = getWidth(this._toolbar.$element()); - const labelPaddings = getOuterWidth($label) - getWidth($label); - $label.css('maxWidth', elementWidth - labelPaddings); + if (!$label.length) { + return; } - _hideOverflowItems() {} + const elementWidth = getWidth(this._toolbar.$element()); + const labelPaddings = getOuterWidth($label) - getWidth($label); + $label.css('maxWidth', elementWidth - labelPaddings); + } + + _hideOverflowItems(): void {} - _dimensionChanged() {} + _dimensionChanged(): void {} - _itemOptionChanged() {} + _itemOptionChanged(): void {} - _optionChanged() {} + _optionChanged(): void {} } diff --git a/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.singleline.ts b/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.singleline.ts index 3b128825e342..c45b2d369548 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.singleline.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/strategy/m_toolbar.singleline.ts @@ -1,11 +1,18 @@ - -import { getWidth } from '../../../core/utils/size'; -import $ from '../../../core/renderer'; -import { each } from '../../../core/utils/iterator'; -import { grep, deferRender } from '../../../core/utils/common'; -import { extend } from '../../../core/utils/extend'; -import DropDownMenu from '../internal/ui.toolbar.menu'; -import { compileGetter } from '../../../core/utils/data'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { + deferRender, + // @ts-expect-error + grep, +} from '@js/core/utils/common'; +import { compileGetter } from '@js/core/utils/data'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { getWidth } from '@js/core/utils/size'; +import type { Item } from '@js/ui/toolbar'; + +import DropDownMenu from '../internal/m_toolbar.menu'; +import type Toolbar from '../m_toolbar'; const INVISIBLE_STATE_CLASS = 'dx-state-invisible'; const TOOLBAR_DROP_DOWN_MENU_CONTAINER_CLASS = 'dx-toolbar-menu-container'; @@ -16,228 +23,243 @@ const TOOLBAR_AUTO_HIDE_ITEM_CLASS = 'dx-toolbar-item-auto-hide'; const TOOLBAR_HIDDEN_ITEM = 'dx-toolbar-item-invisible'; export class SingleLineStrategy { - constructor(toolbar) { - this._toolbar = toolbar; - } + _toolbar: Toolbar; - _initMarkup() { - deferRender(() => { - this._renderOverflowMenu(); - this._renderMenuItems(); - }); - } + _restoreItems?: { + container: dxElementWrapper; + item: dxElementWrapper; + }[]; - _renderOverflowMenu() { - if(!this._hasVisibleMenuItems()) { - return; - } + _menu?: DropDownMenu; - this._renderMenuButtonContainer(); - - const $menu = $('
').appendTo(this._overflowMenuContainer()); - - const itemClickAction = this._toolbar._createActionByOption('onItemClick'); - const menuItemTemplate = this._toolbar._getTemplateByOption('menuItemTemplate'); - - this._menu = this._toolbar._createComponent($menu, DropDownMenu, { - disabled: this._toolbar.option('disabled'), - itemTemplate: () => menuItemTemplate, - onItemClick: (e) => { itemClickAction(e); }, - container: this._toolbar.option('menuContainer'), - onOptionChanged: ({ name, value }) => { - if(name === 'opened') { - this._toolbar.option('overflowMenuVisible', value); - } - if(name === 'items') { - this._updateMenuVisibility(value); - } - }, - }); - } + _$overflowMenuContainer!: dxElementWrapper; - renderMenuItems() { - if(!this._menu) { - this._renderOverflowMenu(); - } + constructor(toolbar) { + this._toolbar = toolbar; + } - this._menu && this._menu.option('items', this._getMenuItems()); + _initMarkup() { + deferRender(() => { + this._renderOverflowMenu(); + this._renderMenuItems(); + }); + } - if(this._menu && !this._menu.option('items').length) { - this._menu.option('opened', false); - } + _renderOverflowMenu() { + if (!this._hasVisibleMenuItems()) { + return; } - _renderMenuButtonContainer() { - this._$overflowMenuContainer = $('
').appendTo(this._toolbar._$afterSection) - .addClass(TOOLBAR_BUTTON_CLASS) - .addClass(TOOLBAR_DROP_DOWN_MENU_CONTAINER_CLASS); - } + this._renderMenuButtonContainer(); - _overflowMenuContainer() { - return this._$overflowMenuContainer; - } + const $menu = $('
').appendTo(this._overflowMenuContainer()); - _updateMenuVisibility(menuItems) { - const items = menuItems ?? this._getMenuItems(); - const isMenuVisible = items.length && this._hasVisibleMenuItems(items); - this._toggleMenuVisibility(isMenuVisible); - } + const itemClickAction = this._toolbar._createActionByOption('onItemClick'); + const menuItemTemplate = this._toolbar._getTemplateByOption('menuItemTemplate'); - _toggleMenuVisibility(value) { - if(!this._overflowMenuContainer()) { - return; + this._menu = this._toolbar._createComponent($menu, DropDownMenu, { + disabled: this._toolbar.option('disabled'), + itemTemplate: () => menuItemTemplate, + onItemClick: (e) => { itemClickAction(e); }, + // @ts-expect-error + container: this._toolbar.option('menuContainer'), + onOptionChanged: ({ name, value }) => { + if (name === 'opened') { + this._toolbar.option('overflowMenuVisible', value); } + if (name === 'items') { + this._updateMenuVisibility(value); + } + }, + }); + } - this._overflowMenuContainer().toggleClass(INVISIBLE_STATE_CLASS, !value); - } - - _renderMenuItems() { - deferRender(() => { - this.renderMenuItems(); - }); + renderMenuItems(): void { + if (!this._menu) { + this._renderOverflowMenu(); } - _dimensionChanged() { - this.renderMenuItems(); + this._menu && this._menu.option('items', this._getMenuItems()); + // @ts-expect-error + if (this._menu && !this._menu.option('items').length) { + this._menu.option('opened', false); } - - _getToolbarItems() { - return grep(this._toolbar.option('items') ?? [], (item) => { - return !this._toolbar._isMenuItem(item); - }); + } + + _renderMenuButtonContainer(): void { + // @ts-expect-error + this._$overflowMenuContainer = $('
').appendTo(this._toolbar._$afterSection) + .addClass(TOOLBAR_BUTTON_CLASS) + .addClass(TOOLBAR_DROP_DOWN_MENU_CONTAINER_CLASS); + } + + _overflowMenuContainer(): dxElementWrapper { + return this._$overflowMenuContainer; + } + + _updateMenuVisibility(menuItems?: Item[]): void { + const items = menuItems ?? this._getMenuItems(); + const isMenuVisible = items.length && this._hasVisibleMenuItems(items); + this._toggleMenuVisibility(isMenuVisible); + } + + _toggleMenuVisibility(value) { + if (!this._overflowMenuContainer()) { + return; } - _getHiddenItems() { - return this._toolbar._itemContainer() - .children(`.${TOOLBAR_AUTO_HIDE_ITEM_CLASS}.${TOOLBAR_HIDDEN_ITEM}`) - .not(`.${INVISIBLE_STATE_CLASS}`); + this._overflowMenuContainer().toggleClass(INVISIBLE_STATE_CLASS, !value); + } + + _renderMenuItems(): void { + deferRender(() => { + this.renderMenuItems(); + }); + } + + _dimensionChanged(): void { + this.renderMenuItems(); + } + + _getToolbarItems() { + return grep(this._toolbar.option('items') ?? [], (item) => !this._toolbar._isMenuItem(item)); + } + + _getHiddenItems() { + return this._toolbar._itemContainer() + .children(`.${TOOLBAR_AUTO_HIDE_ITEM_CLASS}.${TOOLBAR_HIDDEN_ITEM}`) + .not(`.${INVISIBLE_STATE_CLASS}`); + } + + _getMenuItems() { + const menuItems = grep(this._toolbar.option('items') ?? [], (item) => this._toolbar._isMenuItem(item)); + + const $hiddenItems = this._getHiddenItems(); + + this._restoreItems = this._restoreItems ?? []; + + const overflowItems = [].slice.call($hiddenItems).map((hiddenItem) => { + const itemData = this._toolbar._getItemData(hiddenItem); + const $itemContainer = $(hiddenItem); + const $itemMarkup = $itemContainer.children(); + + return extend({ + menuItemTemplate: () => { + // @ts-expect-error + this._restoreItems.push({ + container: $itemContainer, + item: $itemMarkup, + }); + + const $container = $('
').addClass(TOOLBAR_AUTO_HIDE_ITEM_CLASS); + return $container.append($itemMarkup); + }, + }, itemData); + }); + + return [...overflowItems, ...menuItems]; + } + + _hasVisibleMenuItems(items?: any) { + const menuItems = items ?? this._toolbar.option('items'); + let result = false; + + const optionGetter = compileGetter('visible'); + const overflowGetter = compileGetter('locateInMenu'); + + each(menuItems, (index, item) => { + // @ts-expect-error + const itemVisible = optionGetter(item, { functionsAsIs: true }); + // @ts-expect-error + const itemOverflow = overflowGetter(item, { functionsAsIs: true }); + + if (itemVisible !== false && (itemOverflow === 'auto' || itemOverflow === 'always') || item.location === 'menu') { + result = true; + } + }); + + return result; + } + + _arrangeItems(): number { + // @ts-expect-error + this._toolbar._$centerSection.css({ + margin: '0 auto', + float: 'none', + }); + + each(this._restoreItems ?? [], (_, obj) => { + $(obj.container).append(obj.item); + }); + this._restoreItems = []; + + const elementWidth = getWidth(this._toolbar.$element()); + this._hideOverflowItems(elementWidth); + + return elementWidth; + } + + _hideOverflowItems(width?: number) { + // @ts-expect-error + const overflowItems = this._toolbar.$element().find(`.${TOOLBAR_AUTO_HIDE_ITEM_CLASS}`); + + if (!overflowItems.length) { + return; } - _getMenuItems() { - const menuItems = grep(this._toolbar.option('items') ?? [], (item) => { - return this._toolbar._isMenuItem(item); - }); - - const $hiddenItems = this._getHiddenItems(); - - this._restoreItems = this._restoreItems ?? []; - - const overflowItems = [].slice.call($hiddenItems).map((hiddenItem) => { - const itemData = this._toolbar._getItemData(hiddenItem); - const $itemContainer = $(hiddenItem); - const $itemMarkup = $itemContainer.children(); - - return extend({ - menuItemTemplate: () => { - this._restoreItems.push({ - container: $itemContainer, - item: $itemMarkup - }); + const elementWidth = width ?? getWidth(this._toolbar.$element()); + $(overflowItems).removeClass(TOOLBAR_HIDDEN_ITEM); - const $container = $('
').addClass(TOOLBAR_AUTO_HIDE_ITEM_CLASS); - return $container.append($itemMarkup); - } - }, itemData); - }); + let itemsWidth = this._getItemsWidth(); - return [...overflowItems, ...menuItems]; + while (overflowItems.length && elementWidth < itemsWidth) { + const $item = overflowItems.eq(-1); + $item.addClass(TOOLBAR_HIDDEN_ITEM); + itemsWidth = this._getItemsWidth(); + overflowItems.splice(-1, 1); } - - _hasVisibleMenuItems(items) { - const menuItems = items ?? this._toolbar.option('items'); - let result = false; - - const optionGetter = compileGetter('visible'); - const overflowGetter = compileGetter('locateInMenu'); - - each(menuItems, function(index, item) { - const itemVisible = optionGetter(item, { functionsAsIs: true }); - const itemOverflow = overflowGetter(item, { functionsAsIs: true }); - - if(itemVisible !== false && (itemOverflow === 'auto' || itemOverflow === 'always') || item.location === 'menu') { - result = true; - } - }); - - return result; + } + + _getItemsWidth(): number { + // @ts-expect-error + return this._toolbar._getSummaryItemsSize('width', [this._toolbar._$beforeSection, this._toolbar._$centerSection, this._toolbar._$afterSection]); + } + + _itemOptionChanged(item, property, value): void { + if (property === 'disabled' || property === 'options.disabled') { + if (this._toolbar._isMenuItem(item)) { + this._menu?._itemOptionChanged(item, property, value); + return; + } } - _arrangeItems() { - this._toolbar._$centerSection.css({ - margin: '0 auto', - float: 'none' - }); + this.renderMenuItems(); + } - each(this._restoreItems ?? [], function(_, obj) { - $(obj.container).append(obj.item); - }); - this._restoreItems = []; - - const elementWidth = getWidth(this._toolbar.$element()); - this._hideOverflowItems(elementWidth); - - return elementWidth; - } - - _hideOverflowItems(elementWidth) { - const overflowItems = this._toolbar.$element().find(`.${TOOLBAR_AUTO_HIDE_ITEM_CLASS}`); - - if(!overflowItems.length) { - return; - } - - elementWidth = elementWidth ?? getWidth(this._toolbar.$element()); - $(overflowItems).removeClass(TOOLBAR_HIDDEN_ITEM); - - let itemsWidth = this._getItemsWidth(); - - while(overflowItems.length && elementWidth < itemsWidth) { - const $item = overflowItems.eq(-1); - $item.addClass(TOOLBAR_HIDDEN_ITEM); - itemsWidth = this._getItemsWidth(); - overflowItems.splice(-1, 1); - } - } - - _getItemsWidth() { - return this._toolbar._getSummaryItemsSize('width', [this._toolbar._$beforeSection, this._toolbar._$centerSection, this._toolbar._$afterSection]); - } - - _itemOptionChanged(item, property, value) { - if(property === 'disabled' || property === 'options.disabled') { - if(this._toolbar._isMenuItem(item)) { - this._menu?._itemOptionChanged(item, property, value); - return; - } - } - - this.renderMenuItems(); + _renderItem(item, itemElement): void { + if (item.locateInMenu === 'auto') { + itemElement.addClass(TOOLBAR_AUTO_HIDE_ITEM_CLASS); } - - _renderItem(item, itemElement) { - if(item.locateInMenu === 'auto') { - itemElement.addClass(TOOLBAR_AUTO_HIDE_ITEM_CLASS); - } - } - - _optionChanged(name, value) { - switch(name) { - case 'disabled': - this._menu?.option(name, value); - break; - case 'overflowMenuVisible': - this._menu?.option('opened', value); - break; - case 'onItemClick': - this._menu?.option(name, value); - break; - case 'menuContainer': - this._menu?.option('container', value); - break; - case 'menuItemTemplate': - this._menu?.option('itemTemplate', value); - break; - } - + } + + _optionChanged(name, value): void { + // eslint-disable-next-line default-case + switch (name) { + case 'disabled': + this._menu?.option(name, value); + break; + case 'overflowMenuVisible': + this._menu?.option('opened', value); + break; + case 'onItemClick': + this._menu?.option(name, value); + break; + case 'menuContainer': + this._menu?.option('container', value); + break; + case 'menuItemTemplate': + this._menu?.option('itemTemplate', value); + break; } + } } diff --git a/packages/devextreme/js/__internal/ui/widget.ts b/packages/devextreme/js/__internal/ui/widget.ts index c109e02217e6..b6d1f084e8a4 100644 --- a/packages/devextreme/js/__internal/ui/widget.ts +++ b/packages/devextreme/js/__internal/ui/widget.ts @@ -8,6 +8,8 @@ interface AriaOptions { // eslint-disable-next-line spellcheck/spell-checker roledescription?: string; label?: string; + haspopup?: boolean; + expanded?: boolean | undefined; } declare class ExtendedWidget extends Widget { @@ -18,6 +20,8 @@ declare class ExtendedWidget extends Widget { _optionsByReference: Record; + _disposed?: boolean; + setAria(ariaOptions: AriaOptions, $element?: dxElementWrapper): void; setAria( attribute: string, @@ -43,13 +47,19 @@ declare class ExtendedWidget extends Widget { _attachClickEvent(): void; _renderFocusState(): void; + _cleanFocusState(): void; + + _toggleVisibility(visible: boolean): void; // dom_component _render(): void; _initMarkup(): void; + _initTemplates(): void; _clean(): void; _getDefaultOptions(): TProperties; + _getTemplateByOption(optionName: string): unknown; + _getSynchronizableOptionsForCreateComponent(): string[]; _defaultOptionsRules(): Record[]; _optionChanged(args: Record): void; _setOptionWithoutOptionChange(optionName: string, value: unknown): void; @@ -60,7 +70,7 @@ declare class ExtendedWidget extends Widget { _createComponent( element: string | HTMLElement | dxElementWrapper, - component: new (...args) => TComponent, + component: string | (new (...args) => TComponent), config: TComponent extends Component ? TTProperties : never, ): TComponent; @@ -70,6 +80,8 @@ declare class ExtendedWidget extends Widget { _createActionByOption(optionName: string, config?: Record); _isInitialOptionValue(name: string): boolean; _setDeprecatedOptions(): void; + + _dispose(): void; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/devextreme/js/renovation/ui/common/core.d.ts b/packages/devextreme/js/renovation/ui/common/core.d.ts index eb10ff1a1a8e..3cee0c3c41fe 100644 --- a/packages/devextreme/js/renovation/ui/common/core.d.ts +++ b/packages/devextreme/js/renovation/ui/common/core.d.ts @@ -15,8 +15,8 @@ declare module '../../../core/component' { (...args: any[]) => any ); _dispose(): void; - _getDefaultOptions(): Record; - _initOptions(options: Record): void; + _getDefaultOptions(): TProperties; + _initOptions(options: TProperties): void; _init(): void; _initializeComponent(): void; _optionChanging(name: string, value: unknown, prevValue: unknown): void; diff --git a/packages/devextreme/js/ui/popup/ui.popup.js b/packages/devextreme/js/ui/popup/ui.popup.js index ce2024c516be..56ea01447d90 100644 --- a/packages/devextreme/js/ui/popup/ui.popup.js +++ b/packages/devextreme/js/ui/popup/ui.popup.js @@ -28,7 +28,7 @@ import Resizable from '../resizable'; import Button from '../button'; import Overlay from '../overlay/ui.overlay'; import { isMaterialBased, isMaterial, isFluent } from '../themes'; -import '../toolbar/ui.toolbar.base'; +import '../../__internal/ui/toolbar/m_toolbar.base'; import resizeObserverSingleton from '../../core/resize_observer'; import * as zIndexPool from '../../__internal/ui/overlay/m_z_index'; import { PopupPositionController } from './popup_position_controller'; diff --git a/packages/devextreme/js/ui/toolbar.js b/packages/devextreme/js/ui/toolbar.js index b5e0a4e59860..af20910a8cb8 100644 --- a/packages/devextreme/js/ui/toolbar.js +++ b/packages/devextreme/js/ui/toolbar.js @@ -2,8 +2,80 @@ import Toolbar from './toolbar/ui.toolbar'; export default Toolbar; +// STYLE toolbar + /** * @name dxToolbarItem * @inherits CollectionWidgetItem * @type object */ + +/** + * @name dxToolbarOptions.selectedIndex + * @type number + * @default -1 + * @hidden + */ + +/** + * @name dxToolbarOptions.activeStateEnabled + * @hidden + */ + +/** + * @name dxToolbarOptions.focusStateEnabled + * @hidden + */ + +/** + * @name dxToolbarOptions.accessKey + * @hidden + */ + +/** + * @name dxToolbarOptions.tabIndex + * @hidden + */ + +/** + * @name dxToolbarOptions.selectedItems + * @hidden + */ + +/** + * @name dxToolbarOptions.selectedItemKeys + * @hidden + */ + +/** + * @name dxToolbarOptions.keyExpr + * @hidden + */ + +/** + * @name dxToolbarOptions.selectedItem + * @hidden + */ + +/** + * @name dxToolbarOptions.height + * @hidden + */ + +/** + * @name dxToolbarOptions.onSelectionChanged + * @action + * @hidden + */ + +/** + * @name dxToolbar.registerKeyHandler + * @publicName registerKeyHandler(key, handler) + * @hidden + */ + +/** + * @name dxToolbar.focus + * @publicName focus() + * @hidden + */ diff --git a/packages/devextreme/js/ui/toolbar/ui.toolbar.base.js b/packages/devextreme/js/ui/toolbar/ui.toolbar.base.js new file mode 100644 index 000000000000..63025efc2db7 --- /dev/null +++ b/packages/devextreme/js/ui/toolbar/ui.toolbar.base.js @@ -0,0 +1,3 @@ +import ToolbarBase from '../../__internal/ui/toolbar/m_toolbar.base'; + +export default ToolbarBase; diff --git a/packages/devextreme/js/ui/toolbar/ui.toolbar.js b/packages/devextreme/js/ui/toolbar/ui.toolbar.js new file mode 100644 index 000000000000..302b742d2525 --- /dev/null +++ b/packages/devextreme/js/ui/toolbar/ui.toolbar.js @@ -0,0 +1,3 @@ +import Toolbar from '../../__internal/ui/toolbar/m_toolbar'; + +export default Toolbar; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js index d1cdf78f444c..b767d8caa306 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js @@ -46,7 +46,7 @@ const EDITOR_LABEL_CLASS = 'dx-texteditor-label'; const EDITOR_INPUT_CLASS = 'dx-texteditor-input'; const FIELD_ITEM_HELP_TEXT_CLASS = 'dx-field-item-help-text'; -import { TOOLBAR_CLASS } from 'ui/toolbar/constants'; +import { TOOLBAR_CLASS } from '__internal/ui/toolbar/m_constants'; import 'ui/html_editor'; import '../../helpers/ignoreQuillTimers.js'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.menu.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.menu.tests.js index 49be1a4ff879..013ac6314df4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.menu.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.menu.tests.js @@ -4,8 +4,8 @@ import ArrayStore from 'data/array_store'; import fx from 'animation/fx'; import Button from 'ui/button'; import Popup from 'ui/popup'; -import DropDownMenu from 'ui/toolbar/internal/ui.toolbar.menu'; -import ToolbarMenuList from 'ui/toolbar/internal/ui.toolbar.menu.list'; +import DropDownMenu from '__internal/ui/toolbar/internal/m_toolbar.menu'; +import ToolbarMenuList from '__internal/ui/toolbar/internal/m_toolbar.menu.list'; import executeAsyncMock from '../../helpers/executeAsyncMock.js'; import pointerMock from '../../helpers/pointerMock.js'; import keyboardMock from '../../helpers/keyboardMock.js'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.multiline.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.multiline.tests.js index c05e8eb1574e..e96b1a7e1e86 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.multiline.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.multiline.tests.js @@ -5,8 +5,8 @@ import 'ui/select_box'; import { extend } from 'core/utils/extend'; import { value as viewPort } from 'core/utils/view_port'; -import { MultiLineStrategy } from 'ui/toolbar/strategy/toolbar.multiline'; -import { SingleLineStrategy } from 'ui/toolbar/strategy/toolbar.singleline'; +import { MultiLineStrategy } from '__internal/ui/toolbar/strategy/m_toolbar.multiline'; +import { SingleLineStrategy } from '__internal/ui/toolbar/strategy/m_toolbar.singleline'; import 'generic_light.css!'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.tests.js index fe6d70f9b0fb..657b198be3c3 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.tests.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import ToolbarBase from 'ui/toolbar/ui.toolbar.base'; + import fx from 'animation/fx'; import resizeCallbacks from 'core/utils/resize_callbacks'; import themes from 'ui/themes'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js b/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js index b5c3db7ad9fa..60afd27ace4d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js @@ -20,7 +20,7 @@ import DropDownEditor from 'ui/drop_down_editor/ui.drop_down_editor'; import DropDownBox from 'ui/drop_down_box'; import DropDownButton from 'ui/drop_down_button'; import DropDownList from 'ui/drop_down_editor/ui.drop_down_list'; -import DropDownMenu from 'ui/toolbar/internal/ui.toolbar.menu'; +import DropDownMenu from '__internal/ui/toolbar/internal/m_toolbar.menu'; import TextEditor from 'ui/text_box/ui.text_editor'; import Gallery from 'ui/gallery'; import Lookup from 'ui/lookup'; From b7ad910170c32cf19e37d8ab475dd1789f238261 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 11 Jun 2024 11:17:24 +0400 Subject: [PATCH 16/32] DropDownButton, DropDownList, DropDownEditor: move files to TS --- .../ui/drop_down_editor/m_drop_down_button.ts} | 0 .../ui/drop_down_editor/m_drop_down_editor.ts} | 0 .../ui/drop_down_editor/m_drop_down_list.ts} | 0 .../utils.js => __internal/ui/drop_down_editor/m_utils.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/drop_down_editor/ui.drop_down_button.js => __internal/ui/drop_down_editor/m_drop_down_button.ts} (100%) rename packages/devextreme/js/{ui/drop_down_editor/ui.drop_down_editor.js => __internal/ui/drop_down_editor/m_drop_down_editor.ts} (100%) rename packages/devextreme/js/{ui/drop_down_editor/ui.drop_down_list.js => __internal/ui/drop_down_editor/m_drop_down_list.ts} (100%) rename packages/devextreme/js/{ui/drop_down_editor/utils.js => __internal/ui/drop_down_editor/m_utils.ts} (100%) diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_button.js b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts similarity index 100% rename from packages/devextreme/js/ui/drop_down_editor/ui.drop_down_button.js rename to packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts similarity index 100% rename from packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js rename to packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts similarity index 100% rename from packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js rename to packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts diff --git a/packages/devextreme/js/ui/drop_down_editor/utils.js b/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts similarity index 100% rename from packages/devextreme/js/ui/drop_down_editor/utils.js rename to packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts From f6cfb36c7435cd4cd727107c59427ad89830aa7c Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 11 Jun 2024 11:48:16 +0400 Subject: [PATCH 17/32] DropDownButton, DropDownList, DropDownEditor: ignore errors after move to TS --- .../js/__internal/ui/color_box/m_color_box.ts | 2 +- .../__internal/ui/date_box/m_date_box.base.ts | 3 +- .../ui/date_box/m_date_box.strategy.list.ts | 2 +- .../ui/date_range_box/m_date_range_box.ts | 2 +- .../ui/drop_down_editor/m_drop_down_button.ts | 200 +- .../ui/drop_down_editor/m_drop_down_editor.ts | 1674 ++++++++--------- .../ui/drop_down_editor/m_drop_down_list.ts | 1667 ++++++++-------- .../__internal/ui/drop_down_editor/m_utils.ts | 29 +- .../js/__internal/ui/m_drop_down_box.ts | 2 +- .../js/__internal/ui/m_drop_down_button.ts | 2 +- .../devextreme/js/__internal/ui/m_lookup.ts | 2 +- .../drop_down_editor/ui.drop_down_editor.js | 38 + .../ui/drop_down_editor/ui.drop_down_list.js | 13 + .../devextreme/testing/helpers/widgetsList.js | 4 +- 14 files changed, 1828 insertions(+), 1812 deletions(-) create mode 100644 packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js create mode 100644 packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js diff --git a/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts b/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts index 3b991fa7f2b2..7105a26a900d 100644 --- a/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts +++ b/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts @@ -2,7 +2,7 @@ import Color from '@js/color'; import registerComponent from '@js/core/component_registrator'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; -import DropDownEditor from '@js/ui/drop_down_editor/ui.drop_down_editor'; +import DropDownEditor from '@ts/ui/drop_down_editor/m_drop_down_editor'; import ColorView from './m_color_view'; diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts index 0cbf242984d3..b77ceedc7d34 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts @@ -11,7 +11,7 @@ import { isDate as isDateType, isNumeric, isString } from '@js/core/utils/type'; import { getWindow, hasWindow } from '@js/core/utils/window'; import dateLocalization from '@js/localization/date'; import messageLocalization from '@js/localization/message'; -import DropDownEditor from '@js/ui/drop_down_editor/ui.drop_down_editor'; +import DropDownEditor from '@ts/ui/drop_down_editor/m_drop_down_editor'; import Calendar from './m_date_box.strategy.calendar'; import CalendarWithTime from './m_date_box.strategy.calendar_with_time'; @@ -58,7 +58,6 @@ const STRATEGY_CLASSES = { List, }; -// @ts-expect-error const DateBox = DropDownEditor.inherit({ _supportedKeys() { diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts index 2fa80ccab316..04182ee7dc28 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts @@ -7,8 +7,8 @@ import { getHeight, getOuterHeight } from '@js/core/utils/size'; import { isDate } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; import dateLocalization from '@js/localization/date'; -import { getSizeValue } from '@js/ui/drop_down_editor/utils'; import List from '@js/ui/list_light'; +import { getSizeValue } from '@ts/ui/drop_down_editor/m_utils'; import DateBoxStrategy from './m_date_box.strategy'; import dateUtils from './m_date_utils'; diff --git a/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts b/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts index 2e9118fa5993..717e8b3150bd 100644 --- a/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts +++ b/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts @@ -15,9 +15,9 @@ import eventsEngine from '@js/events/core/events_engine'; import { addNamespace } from '@js/events/utils/index'; import messageLocalization from '@js/localization/message'; import type { Properties } from '@js/ui/date_range_box'; -import DropDownButton from '@js/ui/drop_down_editor/ui.drop_down_button'; import Editor from '@js/ui/editor/editor'; import { current, isFluent, isMaterial } from '@js/ui/themes'; +import DropDownButton from '@ts/ui/drop_down_editor/m_drop_down_button'; import ClearButton from '@ts/ui/text_box/m_text_editor.clear'; import TextEditorButtonCollection from '@ts/ui/text_box/texteditor_button_collection/m_index'; diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts index e15a5ec123c8..6a85f9d55e71 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts @@ -1,9 +1,10 @@ -import $ from '../../core/renderer'; -import { extend } from '../../core/utils/extend'; -import eventsEngine from '../../events/core/events_engine'; -import messageLocalization from '../../localization/message'; -import TextEditorButton from '../../__internal/ui/text_box/texteditor_button_collection/m_button'; -import Button from '../button'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import eventsEngine from '@js/events/core/events_engine'; +import messageLocalization from '@js/localization/message'; +import Button from '@js/ui/button'; + +import TextEditorButton from '../text_box/texteditor_button_collection/m_button'; const DROP_DOWN_EDITOR_BUTTON_CLASS = 'dx-dropdowneditor-button'; const DROP_DOWN_EDITOR_BUTTON_VISIBLE = 'dx-dropdowneditor-button-visible'; @@ -11,104 +12,107 @@ const DROP_DOWN_EDITOR_BUTTON_VISIBLE = 'dx-dropdowneditor-button-visible'; const BUTTON_MESSAGE = 'dxDropDownEditor-selectLabel'; export default class DropDownButton extends TextEditorButton { - constructor(name, editor, options) { - super(name, editor, options); - - this.currentTemplate = null; - } - - _attachEvents(instance) { - const { editor } = this; - - instance.option('onClick', (e) => { - if(editor._shouldCallOpenHandler?.()) { - editor._openHandler(e); - return; - } - - !editor.option('openOnFieldClick') && editor._openHandler(e); - }); - - eventsEngine.on(instance.$element(), 'mousedown', (e) => { - if(editor.$element().is('.dx-state-focused')) { - e.preventDefault(); - } - }); - } - - _create() { - const { editor } = this; - const $element = $('
'); - const options = this._getOptions(); - - this._addToContainer($element); - - const instance = editor._createComponent($element, Button, extend({}, options, { elementAttr: { 'aria-label': messageLocalization.format(BUTTON_MESSAGE) } })); - - this._legacyRender(editor.$element(), $element, options.visible); - - return { - $element, - instance - }; - } - - _getOptions() { - const { editor } = this; - const visible = this._isVisible(); - const isReadOnly = editor.option('readOnly'); - const options = { - focusStateEnabled: false, - hoverStateEnabled: false, - activeStateEnabled: false, - useInkRipple: false, - disabled: isReadOnly, - visible - }; - - this._addTemplate(options); - return options; + currentTemplate: any; + + constructor(name, editor, options) { + super(name, editor, options); + + this.currentTemplate = null; + } + + _attachEvents(instance): void { + const { editor } = this; + + instance.option('onClick', (e) => { + if (editor._shouldCallOpenHandler?.()) { + editor._openHandler(e); + return; + } + + !editor.option('openOnFieldClick') && editor._openHandler(e); + }); + + eventsEngine.on(instance.$element(), 'mousedown', (e) => { + if (editor.$element().is('.dx-state-focused')) { + e.preventDefault(); + } + }); + } + + _create() { + const { editor } = this; + const $element = $('
'); + const options = this._getOptions(); + + this._addToContainer($element); + + const instance = editor._createComponent($element, Button, extend({}, options, { elementAttr: { 'aria-label': messageLocalization.format(BUTTON_MESSAGE) } })); + + this._legacyRender(editor.$element(), $element, options.visible); + + return { + $element, + instance, + }; + } + + _getOptions() { + const { editor } = this; + const visible = this._isVisible(); + const isReadOnly = editor.option('readOnly'); + const options = { + focusStateEnabled: false, + hoverStateEnabled: false, + activeStateEnabled: false, + useInkRipple: false, + disabled: isReadOnly, + visible, + }; + + this._addTemplate(options); + return options; + } + + _isVisible(): boolean { + const { editor } = this; + + return super._isVisible() && editor.option('showDropDownButton'); + } + + // TODO: get rid of it + _legacyRender($editor, $element, isVisible) { + $editor.toggleClass(DROP_DOWN_EDITOR_BUTTON_VISIBLE, isVisible); + + if ($element) { + $element + .removeClass('dx-button') + .removeClass('dx-button-mode-contained') + .addClass(DROP_DOWN_EDITOR_BUTTON_CLASS); } + } - _isVisible() { - const { editor } = this; - - return super._isVisible() && editor.option('showDropDownButton'); - } - - // TODO: get rid of it - _legacyRender($editor, $element, isVisible) { - $editor.toggleClass(DROP_DOWN_EDITOR_BUTTON_VISIBLE, isVisible); - - if($element) { - $element - .removeClass('dx-button') - .removeClass('dx-button-mode-contained') - .addClass(DROP_DOWN_EDITOR_BUTTON_CLASS); - } - } - - _isSameTemplate() { - return this.editor.option('dropDownButtonTemplate') === this.currentTemplate; - } + _isSameTemplate() { + return this.editor.option('dropDownButtonTemplate') === this.currentTemplate; + } - _addTemplate(options) { - if(!this._isSameTemplate()) { - options.template = this.editor._getTemplateByOption('dropDownButtonTemplate'); - this.currentTemplate = this.editor.option('dropDownButtonTemplate'); - } + _addTemplate(options): void { + if (!this._isSameTemplate()) { + options.template = this.editor._getTemplateByOption('dropDownButtonTemplate'); + this.currentTemplate = this.editor.option('dropDownButtonTemplate'); } + } - update() { - const shouldUpdate = super.update(); + // @ts-expect-error + update(): void { + const shouldUpdate = super.update(); - if(shouldUpdate) { - const { editor, instance } = this; - const $editor = editor.$element(); - const options = this._getOptions(); + if (shouldUpdate) { + const { editor, instance } = this; + const $editor = editor.$element(); + const options = this._getOptions(); - instance?.option(options); - this._legacyRender($editor, instance?.$element(), options.visible); - } + instance?.option(options); + this._legacyRender($editor, instance?.$element(), options.visible); } + } } diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts index befe1a36a0d7..b1d7c7c4cf3d 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts @@ -1,28 +1,33 @@ -import $ from '../../core/renderer'; -import eventsEngine from '../../events/core/events_engine'; -import Guid from '../../core/guid'; -import registerComponent from '../../core/component_registrator'; -import { noop, splitPair } from '../../core/utils/common'; -import { focused } from '../widget/selectors'; -import { each } from '../../core/utils/iterator'; -import { isDefined } from '../../core/utils/type'; -import { extend } from '../../core/utils/extend'; -import { getPublicElement } from '../../core/element'; -import errors from '../widget/ui.errors'; -import animationPosition from '../../animation/position'; -import { getDefaultAlignment } from '../../core/utils/position'; -import DropDownButton from './ui.drop_down_button'; -import Widget from '../widget/ui.widget'; -import messageLocalization from '../../localization/message'; -import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '../../events/utils/index'; -import TextBox from '../text_box'; -import { name as clickEventName } from '../../events/click'; -import devices from '../../core/devices'; -import { FunctionTemplate } from '../../core/templates/function_template'; -import Popup from '../popup/ui.popup'; -import { hasWindow } from '../../core/utils/window'; -import { getElementWidth, getSizeValue } from './utils'; -import { locate, move } from '../../animation/translator'; +import animationPosition from '@js/animation/position'; +import { locate, move } from '@js/animation/translator'; +import registerComponent from '@js/core/component_registrator'; +import devices from '@js/core/devices'; +import { getPublicElement } from '@js/core/element'; +import Guid from '@js/core/guid'; +import $ from '@js/core/renderer'; +import { FunctionTemplate } from '@js/core/templates/function_template'; +import { + noop, + // @ts-expect-error + splitPair, +} from '@js/core/utils/common'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { getDefaultAlignment } from '@js/core/utils/position'; +import { isDefined } from '@js/core/utils/type'; +import { hasWindow } from '@js/core/utils/window'; +import { name as clickEventName } from '@js/events/click'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '@js/events/utils/index'; +import messageLocalization from '@js/localization/message'; +import Popup from '@js/ui/popup/ui.popup'; +import TextBox from '@js/ui/text_box'; +import { focused } from '@js/ui/widget/selectors'; +import errors from '@js/ui/widget/ui.errors'; +import Widget from '@js/ui/widget/ui.widget'; + +import DropDownButton from './m_drop_down_button'; +import { getElementWidth, getSizeValue } from './m_utils'; const DROP_DOWN_EDITOR_CLASS = 'dx-dropdowneditor'; const DROP_DOWN_EDITOR_INPUT_WRAPPER = 'dx-dropdowneditor-input-wrapper'; @@ -36,940 +41,907 @@ const DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER = 'dx-dropdowneditor-field-templat const OVERLAY_CONTENT_LABEL = 'Dropdown'; const isIOs = devices.current().platform === 'ios'; - +// @ts-expect-error const DropDownEditor = TextBox.inherit({ - _supportedKeys: function() { - return extend({}, this.callBase(), { - tab: function(e) { - if(!this.option('opened')) { - return; - } - - if(!this._popup.getFocusableElements().length) { - this.close(); - return; - } - - const $focusableElement = e.shiftKey - ? this._getLastPopupElement() - : this._getFirstPopupElement(); - - if($focusableElement) { - eventsEngine.trigger($focusableElement, 'focus'); - $focusableElement.select(); - } - e.preventDefault(); - }, - escape: function(e) { - if(this.option('opened')) { - e.preventDefault(); - } - this.close(); - - return true; - }, - upArrow: function(e) { - if(!isCommandKeyPressed(e)) { - e.preventDefault(); - e.stopPropagation(); - if(e.altKey) { - this.close(); - return false; - } - } - return true; - }, - downArrow: function(e) { - if(!isCommandKeyPressed(e)) { - e.preventDefault(); - e.stopPropagation(); - if(e.altKey) { - this._validatedOpening(); - return false; - } - } - return true; - }, - enter: function(e) { - if(this.option('opened')) { - e.preventDefault(); - this._valueChangeEventHandler(e); - } - return true; - } - }); - }, - - _getDefaultButtons: function() { - return this.callBase().concat([{ name: 'dropDown', Ctor: DropDownButton }]); - }, - - _getDefaultOptions: function() { - return extend(this.callBase(), { - value: null, - - onOpened: null, - - onClosed: null, - - opened: false, - - acceptCustomValue: true, - - applyValueMode: 'instantly', - - deferRendering: true, - - activeStateEnabled: true, - - dropDownButtonTemplate: 'dropDownButton', - - fieldTemplate: null, - - openOnFieldClick: false, - - showDropDownButton: true, - - buttons: void 0, - - dropDownOptions: { showTitle: false }, - popupPosition: this._getDefaultPopupPosition(), - onPopupInitialized: null, - applyButtonText: messageLocalization.format('OK'), - cancelButtonText: messageLocalization.format('Cancel'), - buttonsLocation: 'default', - useHiddenSubmitElement: false, - validationMessagePosition: 'auto' - - /** - * @name dxDropDownEditorOptions.mask - * @hidden - */ - - /** - * @name dxDropDownEditorOptions.maskChar - * @hidden - */ - - /** - * @name dxDropDownEditorOptions.maskRules - * @hidden - */ - - /** - * @name dxDropDownEditorOptions.maskInvalidMessage - * @hidden - */ - - /** - * @name dxDropDownEditorOptions.useMaskedValue - * @hidden - */ - - /** - * @name dxDropDownEditorOptions.mode - * @hidden - */ - - /** - * @name dxDropDownEditorOptions.showMaskMode - * @hidden - */ - }); - }, - - _useTemplates: function() { - return true; - }, - - _getDefaultPopupPosition: function(isRtlEnabled) { - const position = getDefaultAlignment(isRtlEnabled); - - return { - offset: { h: 0, v: -1 }, - my: position + ' top', - at: position + ' bottom', - collision: 'flip flip' - }; - }, - - _defaultOptionsRules: function() { - return this.callBase().concat([ - { - device: function(device) { - const isGeneric = device.platform === 'generic'; - return isGeneric; - }, - options: { - popupPosition: { offset: { v: 0 } } - } - } - ]); - }, - - _inputWrapper: function() { - return this.$element().find('.' + DROP_DOWN_EDITOR_INPUT_WRAPPER).first(); - }, - - _init: function() { - this.callBase(); - this._initVisibilityActions(); - this._initPopupInitializedAction(); - this._updatePopupPosition(this.option('rtlEnabled')); - - this._options.cache('dropDownOptions', this.option('dropDownOptions')); - }, - - _updatePopupPosition: function(isRtlEnabled) { - const { my, at } = this._getDefaultPopupPosition(isRtlEnabled); - const currentPosition = this.option('popupPosition'); - - this.option('popupPosition', extend({}, currentPosition, { my, at })); - }, - - _initVisibilityActions: function() { - this._openAction = this._createActionByOption('onOpened', { - excludeValidators: ['disabled', 'readOnly'] - }); - - this._closeAction = this._createActionByOption('onClosed', { - excludeValidators: ['disabled', 'readOnly'] - }); - }, - - _initPopupInitializedAction: function() { - this._popupInitializedAction = this._createActionByOption('onPopupInitialized', { - excludeValidators: ['disabled', 'readOnly'] - }); - }, - - _initMarkup: function() { - this._renderSubmitElement(); - - this.callBase(); - - this.$element() - .addClass(DROP_DOWN_EDITOR_CLASS); - - this.setAria('role', this._getAriaRole()); - }, - - _render: function() { - this.callBase(); - - this._renderOpenHandler(); - this._attachFocusOutHandler(); - this._renderOpenedState(); - }, - - _renderContentImpl: function() { - if(!this.option('deferRendering')) { - this._createPopup(); + _supportedKeys() { + return extend({}, this.callBase(), { + tab(e) { + if (!this.option('opened')) { + return; } - }, - - _renderInput: function() { - this.callBase(); - this._renderTemplateWrapper(); - - this._wrapInput(); - this._setDefaultAria(); - }, - - _wrapInput: function() { - this._$container = this.$element() - .wrapInner($('
').addClass(DROP_DOWN_EDITOR_INPUT_WRAPPER)) - .children() - .eq(0); - }, - - _getAriaHasPopup() { - return 'true'; - }, - - _getAriaAutocomplete() { - return 'none'; - }, - - _getAriaRole() { - return 'combobox'; - }, - - _setDefaultAria: function() { - this.setAria({ - 'haspopup': this._getAriaHasPopup(), - 'autocomplete': this._getAriaAutocomplete(), - 'role': this._getAriaRole(), - }); - }, - - _readOnlyPropValue: function() { - return !this._isEditable() || this.callBase(); - }, - _cleanFocusState: function() { - this.callBase(); - - if(this.option('fieldTemplate')) { - this._detachFocusEvents(); + if (!this._popup.getFocusableElements().length) { + this.close(); + return; } - }, - _getFieldTemplate: function() { - return this.option('fieldTemplate') && this._getTemplateByOption('fieldTemplate'); - }, + const $focusableElement = e.shiftKey + ? this._getLastPopupElement() + : this._getFirstPopupElement(); - _renderMask: function() { - if(this.option('fieldTemplate')) { - return; + if ($focusableElement) { + // @ts-expect-error + eventsEngine.trigger($focusableElement, 'focus'); + $focusableElement.select(); + } + e.preventDefault(); + }, + escape(e) { + if (this.option('opened')) { + e.preventDefault(); } - this.callBase(); - }, - - _renderField: function() { - const fieldTemplate = this._getFieldTemplate(); - - fieldTemplate && this._renderTemplatedField(fieldTemplate, this._fieldRenderData()); - }, - - _renderPlaceholder: function() { - const hasFieldTemplate = !!this._getFieldTemplate(); + this.close(); - if(!hasFieldTemplate) { - this.callBase(); + return true; + }, + upArrow(e) { + if (!isCommandKeyPressed(e)) { + e.preventDefault(); + e.stopPropagation(); + if (e.altKey) { + this.close(); + return false; + } } - }, - - _renderValue: function() { - if(this.option('useHiddenSubmitElement')) { - this._setSubmitValue(); + return true; + }, + downArrow(e) { + if (!isCommandKeyPressed(e)) { + e.preventDefault(); + e.stopPropagation(); + if (e.altKey) { + this._validatedOpening(); + return false; + } } - - const promise = this.callBase(); - - promise.always(this._renderField.bind(this)); - }, - - _getButtonsContainer() { - const fieldTemplate = this._getFieldTemplate(); - return fieldTemplate ? this._$container : this._$textEditorContainer; - }, - - _renderTemplateWrapper() { - const fieldTemplate = this._getFieldTemplate(); - if(!fieldTemplate) { - return; + return true; + }, + enter(e) { + if (this.option('opened')) { + e.preventDefault(); + this._valueChangeEventHandler(e); } + return true; + }, + }); + }, + + _getDefaultButtons() { + return this.callBase().concat([{ name: 'dropDown', Ctor: DropDownButton }]); + }, + + _getDefaultOptions() { + return extend(this.callBase(), { + value: null, + onOpened: null, + onClosed: null, + opened: false, + acceptCustomValue: true, + applyValueMode: 'instantly', + deferRendering: true, + activeStateEnabled: true, + dropDownButtonTemplate: 'dropDownButton', + fieldTemplate: null, + openOnFieldClick: false, + showDropDownButton: true, + // eslint-disable-next-line no-void + buttons: void 0, + dropDownOptions: { showTitle: false }, + popupPosition: this._getDefaultPopupPosition(), + onPopupInitialized: null, + applyButtonText: messageLocalization.format('OK'), + cancelButtonText: messageLocalization.format('Cancel'), + buttonsLocation: 'default', + useHiddenSubmitElement: false, + validationMessagePosition: 'auto', + }); + }, + + _useTemplates() { + return true; + }, + + _getDefaultPopupPosition(isRtlEnabled) { + const position = getDefaultAlignment(isRtlEnabled); + + return { + offset: { h: 0, v: -1 }, + my: `${position} top`, + at: `${position} bottom`, + collision: 'flip flip', + }; + }, + + _defaultOptionsRules() { + return this.callBase().concat([ + { + device(device) { + const isGeneric = device.platform === 'generic'; + return isGeneric; + }, + options: { + popupPosition: { offset: { v: 0 } }, + }, + }, + ]); + }, + + _inputWrapper() { + return this.$element().find(`.${DROP_DOWN_EDITOR_INPUT_WRAPPER}`).first(); + }, + + _init() { + this.callBase(); + this._initVisibilityActions(); + this._initPopupInitializedAction(); + this._updatePopupPosition(this.option('rtlEnabled')); + + this._options.cache('dropDownOptions', this.option('dropDownOptions')); + }, + + _updatePopupPosition(isRtlEnabled) { + const { my, at } = this._getDefaultPopupPosition(isRtlEnabled); + const currentPosition = this.option('popupPosition'); + + this.option('popupPosition', extend({}, currentPosition, { my, at })); + }, + + _initVisibilityActions() { + this._openAction = this._createActionByOption('onOpened', { + excludeValidators: ['disabled', 'readOnly'], + }); + + this._closeAction = this._createActionByOption('onClosed', { + excludeValidators: ['disabled', 'readOnly'], + }); + }, + + _initPopupInitializedAction() { + this._popupInitializedAction = this._createActionByOption('onPopupInitialized', { + excludeValidators: ['disabled', 'readOnly'], + }); + }, + + _initMarkup() { + this._renderSubmitElement(); + + this.callBase(); + + this.$element() + .addClass(DROP_DOWN_EDITOR_CLASS); + + this.setAria('role', this._getAriaRole()); + }, + + _render() { + this.callBase(); + + this._renderOpenHandler(); + this._attachFocusOutHandler(); + this._renderOpenedState(); + }, + + _renderContentImpl() { + if (!this.option('deferRendering')) { + this._createPopup(); + } + }, + + _renderInput() { + this.callBase(); + this._renderTemplateWrapper(); + + this._wrapInput(); + this._setDefaultAria(); + }, + + _wrapInput() { + this._$container = this.$element() + .wrapInner($('
').addClass(DROP_DOWN_EDITOR_INPUT_WRAPPER)) + .children() + .eq(0); + }, + + _getAriaHasPopup() { + return 'true'; + }, + + _getAriaAutocomplete() { + return 'none'; + }, + + _getAriaRole() { + return 'combobox'; + }, + + _setDefaultAria() { + this.setAria({ + haspopup: this._getAriaHasPopup(), + autocomplete: this._getAriaAutocomplete(), + role: this._getAriaRole(), + }); + }, + + _readOnlyPropValue() { + return !this._isEditable() || this.callBase(); + }, + + _cleanFocusState() { + this.callBase(); + + if (this.option('fieldTemplate')) { + this._detachFocusEvents(); + } + }, - if(!this._$templateWrapper) { - this._$templateWrapper = $('
') - .addClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER) - .prependTo(this.$element()); - } - }, + _getFieldTemplate() { + return this.option('fieldTemplate') && this._getTemplateByOption('fieldTemplate'); + }, - _renderTemplatedField: function(fieldTemplate, data) { - const isFocused = focused(this._input()); + _renderMask() { + if (this.option('fieldTemplate')) { + return; + } - this._detachKeyboardEvents(); - this._detachFocusEvents(); + this.callBase(); + }, - this._$textEditorContainer.remove(); - this._$templateWrapper.empty(); + _renderField() { + const fieldTemplate = this._getFieldTemplate(); - const $templateWrapper = this._$templateWrapper; - fieldTemplate.render({ - model: data, - container: getPublicElement($templateWrapper), - onRendered: () => { - const isRenderedInRoot = !!this.$element().find($templateWrapper).length; + fieldTemplate && this._renderTemplatedField(fieldTemplate, this._fieldRenderData()); + }, - if(!isRenderedInRoot) { - return; - } + _renderPlaceholder() { + const hasFieldTemplate = !!this._getFieldTemplate(); - const $input = this._input(); + if (!hasFieldTemplate) { + this.callBase(); + } + }, - if(!$input.length) { - throw errors.Error('E1010'); - } + _renderValue() { + if (this.option('useHiddenSubmitElement')) { + this._setSubmitValue(); + } - this._integrateInput(); + const promise = this.callBase(); - isFocused && eventsEngine.trigger($input, 'focus'); - } - }); - }, - - _integrateInput: function() { - const { isValid } = this.option(); - - this._renderFocusState(); - this._refreshValueChangeEvent(); - this._refreshEvents(); - this._refreshEmptinessEvent(); - this._setDefaultAria(); - this._setFieldAria(); - this._toggleValidationClasses(!isValid); - this.option('_onMarkupRendered')?.(); - }, - - _refreshEmptinessEvent: function() { - eventsEngine.off(this._input(), 'input blur', this._toggleEmptinessEventHandler); - this._renderEmptinessEvent(); - }, - - _fieldRenderData: function() { - return this.option('value'); - }, - - _initTemplates: function() { - this._templateManager.addDefaultTemplates({ - dropDownButton: new FunctionTemplate(function(options) { - const $icon = $('
').addClass(DROP_DOWN_EDITOR_BUTTON_ICON); - $(options.container).append($icon); - }) - }); - this.callBase(); - }, + promise.always(this._renderField.bind(this)); + }, - _renderOpenHandler: function() { - const $inputWrapper = this._inputWrapper(); - const eventName = addNamespace(clickEventName, this.NAME); - const openOnFieldClick = this.option('openOnFieldClick'); + _getButtonsContainer() { + const fieldTemplate = this._getFieldTemplate(); + return fieldTemplate ? this._$container : this._$textEditorContainer; + }, - eventsEngine.off($inputWrapper, eventName); - eventsEngine.on($inputWrapper, eventName, this._getInputClickHandler(openOnFieldClick)); - this.$element().toggleClass(DROP_DOWN_EDITOR_FIELD_CLICKABLE, openOnFieldClick); + _renderTemplateWrapper() { + const fieldTemplate = this._getFieldTemplate(); + if (!fieldTemplate) { + return; + } - if(openOnFieldClick) { - this._openOnFieldClickAction = this._createAction(this._openHandler.bind(this)); - } - }, - - _attachFocusOutHandler: function() { - if(isIOs) { - this._detachFocusOutEvents(); - eventsEngine.on(this._inputWrapper(), addNamespace('focusout', this.NAME), (event) => { - const newTarget = event.relatedTarget; - if(newTarget && this.option('opened')) { - const isNewTargetOutside = this._isTargetOutOfComponent(newTarget); - if(isNewTargetOutside) { - this.close(); - } - } - }); + if (!this._$templateWrapper) { + this._$templateWrapper = $('
') + .addClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER) + .prependTo(this.$element()); + } + }, + + _renderTemplatedField(fieldTemplate, data) { + const isFocused = focused(this._input()); + + this._detachKeyboardEvents(); + this._detachFocusEvents(); + + this._$textEditorContainer.remove(); + this._$templateWrapper.empty(); + + const $templateWrapper = this._$templateWrapper; + fieldTemplate.render({ + model: data, + container: getPublicElement($templateWrapper), + onRendered: () => { + const isRenderedInRoot = !!this.$element().find($templateWrapper).length; + + if (!isRenderedInRoot) { + return; + } + + const $input = this._input(); + + if (!$input.length) { + throw errors.Error('E1010'); + } + + this._integrateInput(); + // @ts-expect-error + isFocused && eventsEngine.trigger($input, 'focus'); + }, + }); + }, + + _integrateInput() { + const { isValid } = this.option(); + + this._renderFocusState(); + this._refreshValueChangeEvent(); + this._refreshEvents(); + this._refreshEmptinessEvent(); + this._setDefaultAria(); + this._setFieldAria(); + this._toggleValidationClasses(!isValid); + this.option('_onMarkupRendered')?.(); + }, + + _refreshEmptinessEvent() { + eventsEngine.off(this._input(), 'input blur', this._toggleEmptinessEventHandler); + this._renderEmptinessEvent(); + }, + + _fieldRenderData() { + return this.option('value'); + }, + + _initTemplates() { + this._templateManager.addDefaultTemplates({ + // @ts-expect-error + dropDownButton: new FunctionTemplate((options) => { + const $icon = $('
').addClass(DROP_DOWN_EDITOR_BUTTON_ICON); + $(options.container).append($icon); + }), + }); + this.callBase(); + }, + + _renderOpenHandler() { + const $inputWrapper = this._inputWrapper(); + const eventName = addNamespace(clickEventName, this.NAME); + const openOnFieldClick = this.option('openOnFieldClick'); + + eventsEngine.off($inputWrapper, eventName); + eventsEngine.on($inputWrapper, eventName, this._getInputClickHandler(openOnFieldClick)); + this.$element().toggleClass(DROP_DOWN_EDITOR_FIELD_CLICKABLE, openOnFieldClick); + + if (openOnFieldClick) { + this._openOnFieldClickAction = this._createAction(this._openHandler.bind(this)); + } + }, + + _attachFocusOutHandler() { + if (isIOs) { + this._detachFocusOutEvents(); + eventsEngine.on(this._inputWrapper(), addNamespace('focusout', this.NAME), (event) => { + const newTarget = event.relatedTarget; + if (newTarget && this.option('opened')) { + const isNewTargetOutside = this._isTargetOutOfComponent(newTarget); + if (isNewTargetOutside) { + this.close(); + } } - }, + }); + } + }, - _isTargetOutOfComponent: function(newTarget) { - const popupWrapper = this.content ? $(this.content()).closest(`.${DROP_DOWN_EDITOR_OVERLAY}`) : this._$popup; - const isTargetOutsidePopup = $(newTarget).closest(`.${DROP_DOWN_EDITOR_OVERLAY}`, popupWrapper).length === 0; + _isTargetOutOfComponent(newTarget) { + const popupWrapper = this.content ? $(this.content()).closest(`.${DROP_DOWN_EDITOR_OVERLAY}`) : this._$popup; + // @ts-expect-error + const isTargetOutsidePopup = $(newTarget).closest(`.${DROP_DOWN_EDITOR_OVERLAY}`, popupWrapper).length === 0; - return isTargetOutsidePopup; - }, + return isTargetOutsidePopup; + }, - _detachFocusOutEvents: function() { - isIOs && eventsEngine.off(this._inputWrapper(), addNamespace('focusout', this.NAME)); - }, + _detachFocusOutEvents() { + isIOs && eventsEngine.off(this._inputWrapper(), addNamespace('focusout', this.NAME)); + }, - _getInputClickHandler: function(openOnFieldClick) { - return openOnFieldClick ? - (e) => { this._executeOpenAction(e); } : - (e) => { this._focusInput(); }; - }, + _getInputClickHandler(openOnFieldClick) { + return openOnFieldClick + ? (e) => { this._executeOpenAction(e); } + : () => { this._focusInput(); }; + }, - _openHandler: function() { - this._toggleOpenState(); - }, + _openHandler() { + this._toggleOpenState(); + }, - _executeOpenAction: function(e) { - this._openOnFieldClickAction({ event: e }); - }, + _executeOpenAction(e) { + this._openOnFieldClickAction({ event: e }); + }, - _keyboardEventBindingTarget: function() { - return this._input(); - }, + _keyboardEventBindingTarget() { + return this._input(); + }, - _focusInput: function() { - if(this.option('disabled')) { - return false; - } + _focusInput() { + if (this.option('disabled')) { + return false; + } - if(this.option('focusStateEnabled') && !focused(this._input())) { - this._resetCaretPosition(); + if (this.option('focusStateEnabled') && !focused(this._input())) { + this._resetCaretPosition(); + // @ts-expect-error + eventsEngine.trigger(this._input(), 'focus'); + } - eventsEngine.trigger(this._input(), 'focus'); - } + return true; + }, - return true; - }, + _resetCaretPosition(ignoreEditable = false) { + const inputElement = this._input().get(0); - _resetCaretPosition: function(ignoreEditable = false) { - const inputElement = this._input().get(0); + if (inputElement) { + const { value } = inputElement; + const caretPosition = isDefined(value) && (ignoreEditable || this._isEditable()) ? value.length : 0; - if(inputElement) { - const { value } = inputElement; - const caretPosition = isDefined(value) && (ignoreEditable || this._isEditable()) ? value.length : 0; + this._caret({ start: caretPosition, end: caretPosition }, true); + } + }, - this._caret({ start: caretPosition, end: caretPosition }, true); - } - }, + _isEditable() { + return this.option('acceptCustomValue'); + }, - _isEditable: function() { - return this.option('acceptCustomValue'); - }, + _toggleOpenState(isVisible) { + if (!this._focusInput()) { + return; + } - _toggleOpenState: function(isVisible) { - if(!this._focusInput()) { - return; - } + if (!this.option('readOnly')) { + isVisible = arguments.length ? isVisible : !this.option('opened'); + this.option('opened', isVisible); + } + }, - if(!this.option('readOnly')) { - isVisible = arguments.length ? isVisible : !this.option('opened'); - this.option('opened', isVisible); - } - }, + _getControlsAria() { + return this._popup && this._popupContentId; + }, - _getControlsAria() { - return this._popup && this._popupContentId; - }, + _renderOpenedState() { + const opened = this.option('opened'); - _renderOpenedState: function() { - const opened = this.option('opened'); + if (opened) { + this._createPopup(); + } - if(opened) { - this._createPopup(); - } + this.$element().toggleClass(DROP_DOWN_EDITOR_ACTIVE, opened); + this._setPopupOption('visible', opened); - this.$element().toggleClass(DROP_DOWN_EDITOR_ACTIVE, opened); - this._setPopupOption('visible', opened); + const arias = { + expanded: opened, + controls: this._getControlsAria(), + }; - const arias = { - 'expanded': opened, - 'controls': this._getControlsAria(), - }; + this.setAria(arias); + this.setAria('owns', (opened || undefined) && this._popupContentId, this.$element()); + }, - this.setAria(arias); - this.setAria('owns', ((opened || undefined) && this._popupContentId), this.$element()); - }, + _createPopup() { + if (this._$popup) { + return; + } - _createPopup: function() { - if(this._$popup) { - return; - } + this._$popup = $('
').addClass(DROP_DOWN_EDITOR_OVERLAY) + .appendTo(this.$element()); - this._$popup = $('
').addClass(DROP_DOWN_EDITOR_OVERLAY) - .appendTo(this.$element()); + this._renderPopup(); + this._renderPopupContent(); + this._setPopupAriaLabel(); + }, - this._renderPopup(); - this._renderPopupContent(); - this._setPopupAriaLabel(); - }, + _setPopupAriaLabel() { + const $overlayContent = this._popup.$overlayContent(); - _setPopupAriaLabel() { - const $overlayContent = this._popup.$overlayContent(); + this.setAria('label', OVERLAY_CONTENT_LABEL, $overlayContent); + }, - this.setAria('label', OVERLAY_CONTENT_LABEL, $overlayContent); - }, + _renderPopupContent: noop, - _renderPopupContent: noop, + _renderPopup() { + const popupConfig = extend(this._popupConfig(), this._options.cache('dropDownOptions')); - _renderPopup: function() { - const popupConfig = extend(this._popupConfig(), this._options.cache('dropDownOptions')); + delete popupConfig.closeOnOutsideClick; - delete popupConfig.closeOnOutsideClick; + this._popup = this._createComponent(this._$popup, Popup, popupConfig); - this._popup = this._createComponent(this._$popup, Popup, popupConfig); + this._popup.on({ + showing: this._popupShowingHandler.bind(this), + shown: this._popupShownHandler.bind(this), + hiding: this._popupHidingHandler.bind(this), + hidden: this._popupHiddenHandler.bind(this), + contentReady: this._contentReadyHandler.bind(this), + }); - this._popup.on({ - 'showing': this._popupShowingHandler.bind(this), - 'shown': this._popupShownHandler.bind(this), - 'hiding': this._popupHidingHandler.bind(this), - 'hidden': this._popupHiddenHandler.bind(this), - 'contentReady': this._contentReadyHandler.bind(this) - }); + this._attachPopupKeyHandler(); - this._attachPopupKeyHandler(); + this._contentReadyHandler(); - this._contentReadyHandler(); + this._setPopupContentId(this._popup.$content()); - this._setPopupContentId(this._popup.$content()); + this._bindInnerWidgetOptions(this._popup, 'dropDownOptions'); + }, - this._bindInnerWidgetOptions(this._popup, 'dropDownOptions'); - }, + _attachPopupKeyHandler() { + eventsEngine.on(this._popup.$overlayContent(), addNamespace('keydown', this.NAME), (e) => this._popupKeyHandler(e)); + }, - _attachPopupKeyHandler() { - eventsEngine.on(this._popup.$overlayContent(), addNamespace('keydown', this.NAME), (e) => this._popupKeyHandler(e)); - }, + _popupKeyHandler(e) { + // eslint-disable-next-line default-case, @typescript-eslint/switch-exhaustiveness-check + switch (normalizeKeyName(e)) { + case 'tab': + this._popupTabHandler(e); + break; + case 'escape': + this._popupEscHandler(e); + break; + } + }, - _popupKeyHandler(e) { - switch(normalizeKeyName(e)) { - case 'tab': - this._popupTabHandler(e); - break; - case 'escape': - this._popupEscHandler(e); - break; - } - }, + _popupTabHandler(e) { + const $target = $(e.target); + const moveBackward = e.shiftKey && $target.is(this._getFirstPopupElement()); + const moveForward = !e.shiftKey && $target.is(this._getLastPopupElement()); - _popupTabHandler(e) { - const $target = $(e.target); - const moveBackward = e.shiftKey && $target.is(this._getFirstPopupElement()); - const moveForward = !e.shiftKey && $target.is(this._getLastPopupElement()); + if (moveForward || moveBackward) { + // @ts-expect-error + eventsEngine.trigger(this.field(), 'focus'); + e.preventDefault(); + } + }, + + _popupEscHandler() { + // @ts-expect-error + eventsEngine.trigger(this._input(), 'focus'); + this.close(); + }, + + _setPopupContentId($popupContent) { + this._popupContentId = `dx-${new Guid()}`; + this.setAria('id', this._popupContentId, $popupContent); + }, + + _contentReadyHandler: noop, + + _popupConfig() { + return { + onInitialized: this._getPopupInitializedHandler(), + position: extend(this.option('popupPosition'), { + of: this.$element(), + }), + showTitle: this.option('dropDownOptions.showTitle'), + _ignoreFunctionValueDeprecation: true, + width: () => getElementWidth(this.$element()), + height: 'auto', + shading: false, + hideOnParentScroll: true, + hideOnOutsideClick: (e) => this._closeOutsideDropDownHandler(e), + animation: { + show: { + type: 'fade', duration: 0, from: 0, to: 1, + }, + hide: { + type: 'fade', duration: 400, from: 1, to: 0, + }, + }, + deferRendering: false, + focusStateEnabled: false, + showCloseButton: false, + dragEnabled: false, + toolbarItems: this._getPopupToolbarItems(), + onPositioned: this._popupPositionedHandler.bind(this), + fullScreen: false, + contentTemplate: null, + _hideOnParentScrollTarget: this.$element(), + _wrapperClassExternal: DROP_DOWN_EDITOR_OVERLAY, + _ignorePreventScrollEventsDeprecation: true, + }; + }, + + _popupInitializedHandler: noop, + + _getPopupInitializedHandler() { + const onPopupInitialized = this.option('onPopupInitialized'); + + return (e) => { + this._popupInitializedHandler(e); + if (onPopupInitialized) { + this._popupInitializedAction({ popup: e.component }); + } + }; + }, + + _dimensionChanged() { + // TODO: Use ResizeObserver to hide popup after editor visibility change + // instead of window's dimension change, + if (hasWindow() && !this.$element().is(':visible')) { + this.close(); + return; + } - if(moveForward || moveBackward) { - eventsEngine.trigger(this.field(), 'focus'); - e.preventDefault(); - } - }, + this._updatePopupWidth(); + }, - _popupEscHandler() { - eventsEngine.trigger(this._input(), 'focus'); - this.close(); - }, - - _setPopupContentId($popupContent) { - this._popupContentId = 'dx-' + new Guid(); - this.setAria('id', this._popupContentId, $popupContent); - }, - - _contentReadyHandler: noop, - - _popupConfig: function() { - - return { - onInitialized: this._getPopupInitializedHandler(), - position: extend(this.option('popupPosition'), { - of: this.$element() - }), - showTitle: this.option('dropDownOptions.showTitle'), - _ignoreFunctionValueDeprecation: true, - width: () => getElementWidth(this.$element()), - height: 'auto', - shading: false, - hideOnParentScroll: true, - hideOnOutsideClick: (e) => this._closeOutsideDropDownHandler(e), - animation: { - show: { type: 'fade', duration: 0, from: 0, to: 1 }, - hide: { type: 'fade', duration: 400, from: 1, to: 0 } - }, - deferRendering: false, - focusStateEnabled: false, - showCloseButton: false, - dragEnabled: false, - toolbarItems: this._getPopupToolbarItems(), - onPositioned: this._popupPositionedHandler.bind(this), - fullScreen: false, - contentTemplate: null, - _hideOnParentScrollTarget: this.$element(), - _wrapperClassExternal: DROP_DOWN_EDITOR_OVERLAY, - _ignorePreventScrollEventsDeprecation: true, - }; - }, - - _popupInitializedHandler: noop, - - _getPopupInitializedHandler: function() { - const onPopupInitialized = this.option('onPopupInitialized'); - - return (e) => { - this._popupInitializedHandler(e); - if(onPopupInitialized) { - this._popupInitializedAction({ popup: e.component }); - } - }; - }, - - _dimensionChanged: function() { - // TODO: Use ResizeObserver to hide popup after editor visibility change - // instead of window's dimension change, - if(hasWindow() && !this.$element().is(':visible')) { - this.close(); - return; - } + _updatePopupWidth() { + const popupWidth = getSizeValue(this.option('dropDownOptions.width')); - this._updatePopupWidth(); - }, + if (popupWidth === undefined) { + this._setPopupOption('width', () => getElementWidth(this.$element())); + } + }, - _updatePopupWidth: function() { - const popupWidth = getSizeValue(this.option('dropDownOptions.width')); + _popupPositionedHandler(e) { + const { labelMode, stylingMode } = this.option(); - if(popupWidth === undefined) { - this._setPopupOption('width', () => getElementWidth(this.$element())); - } - }, + if (!this._popup) { + return; + } - _popupPositionedHandler: function(e) { - const { labelMode, stylingMode } = this.option(); + const $popupOverlayContent = this._popup.$overlayContent(); + const isOverlayFlipped = e.position?.v?.flip; + const shouldIndentForLabel = labelMode !== 'hidden' && labelMode !== 'outside' && stylingMode === 'outlined'; - if(!this._popup) { - return; - } + if (e.position) { + $popupOverlayContent.toggleClass(DROP_DOWN_EDITOR_OVERLAY_FLIPPED, isOverlayFlipped); + } - const $popupOverlayContent = this._popup.$overlayContent(); - const isOverlayFlipped = e.position?.v?.flip; - const shouldIndentForLabel = labelMode !== 'hidden' && labelMode !== 'outside' && stylingMode === 'outlined'; + if (isOverlayFlipped && shouldIndentForLabel && this._label.isVisible()) { + const $label = this._label.$element(); - if(e.position) { - $popupOverlayContent.toggleClass(DROP_DOWN_EDITOR_OVERLAY_FLIPPED, isOverlayFlipped); - } + move($popupOverlayContent, { + // eslint-disable-next-line radix + top: locate($popupOverlayContent).top - parseInt($label.css('fontSize')), + }); + } + }, - if(isOverlayFlipped && shouldIndentForLabel && this._label.isVisible()) { - const $label = this._label.$element(); + _popupShowingHandler: noop, - move($popupOverlayContent, { - top: locate($popupOverlayContent).top - parseInt($label.css('fontSize')) - }); - } - }, + _popupHidingHandler() { + this.option('opened', false); + }, - _popupShowingHandler: noop, + _popupShownHandler() { + this._openAction(); - _popupHidingHandler: function() { - this.option('opened', false); - }, + this._validationMessage?.option('positionSide', this._getValidationMessagePositionSide()); + }, - _popupShownHandler: function() { - this._openAction(); + _popupHiddenHandler() { + this._closeAction(); + this._validationMessage?.option('positionSide', this._getValidationMessagePositionSide()); + }, - this._validationMessage?.option('positionSide', this._getValidationMessagePositionSide()); - }, + _getValidationMessagePositionSide() { + const validationMessagePosition = this.option('validationMessagePosition'); - _popupHiddenHandler: function() { - this._closeAction(); - this._validationMessage?.option('positionSide', this._getValidationMessagePositionSide()); - }, + if (validationMessagePosition !== 'auto') { + return validationMessagePosition; + } - _getValidationMessagePositionSide: function() { - const validationMessagePosition = this.option('validationMessagePosition'); + let positionSide = 'bottom'; - if(validationMessagePosition !== 'auto') { - return validationMessagePosition; - } + if (this._popup && this._popup.option('visible')) { + // @ts-expect-error + const { top: myTop } = animationPosition.setup(this.$element()); + // @ts-expect-error + const { top: popupTop } = animationPosition.setup(this._popup.$content()); - let positionSide = 'bottom'; + positionSide = (myTop + this.option('popupPosition').offset.v) > popupTop ? 'bottom' : 'top'; + } - if(this._popup && this._popup.option('visible')) { - const { top: myTop } = animationPosition.setup(this.$element()); - const { top: popupTop } = animationPosition.setup(this._popup.$content()); + return positionSide; + }, - positionSide = (myTop + this.option('popupPosition').offset.v) > popupTop ? 'bottom' : 'top'; - } + _closeOutsideDropDownHandler({ target }) { + const $target = $(target); + const dropDownButton = this.getButton('dropDown'); + const $dropDownButton = dropDownButton && dropDownButton.$element(); + const isInputClicked = !!$target.closest(this.$element()).length; + const isDropDownButtonClicked = !!$target.closest($dropDownButton).length; + const isOutsideClick = !isInputClicked && !isDropDownButtonClicked; - return positionSide; - }, + return isOutsideClick; + }, - _closeOutsideDropDownHandler: function({ target }) { - const $target = $(target); - const dropDownButton = this.getButton('dropDown'); - const $dropDownButton = dropDownButton && dropDownButton.$element(); - const isInputClicked = !!$target.closest(this.$element()).length; - const isDropDownButtonClicked = !!$target.closest($dropDownButton).length; - const isOutsideClick = !isInputClicked && !isDropDownButtonClicked; + _clean() { + delete this._openOnFieldClickAction; + delete this._$templateWrapper; - return isOutsideClick; - }, + if (this._$popup) { + this._$popup.remove(); + delete this._$popup; + delete this._popup; + } + this.callBase(); + }, - _clean: function() { - delete this._openOnFieldClickAction; - delete this._$templateWrapper; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _setPopupOption(optionName, value) { + this._setWidgetOption('_popup', arguments); + }, - if(this._$popup) { - this._$popup.remove(); - delete this._$popup; - delete this._popup; - } - this.callBase(); - }, + _validatedOpening() { + if (!this.option('readOnly')) { + this._toggleOpenState(true); + } + }, + + _getPopupToolbarItems() { + return this.option('applyValueMode') === 'useButtons' + ? this._popupToolbarItemsConfig() + : []; + }, + + _getFirstPopupElement() { + return $(this._popup.getFocusableElements()).first(); + }, + + _getLastPopupElement() { + return $(this._popup.getFocusableElements()).last(); + }, + + _popupToolbarItemsConfig() { + const buttonsConfig = [ + { + shortcut: 'done', + options: { + onClick: this._applyButtonHandler.bind(this), + text: this.option('applyButtonText'), + }, + }, + { + shortcut: 'cancel', + options: { + onClick: this._cancelButtonHandler.bind(this), + text: this.option('cancelButtonText'), + }, + }, + ]; + + return this._applyButtonsLocation(buttonsConfig); + }, + + _applyButtonsLocation(buttonsConfig) { + const buttonsLocation = this.option('buttonsLocation'); + const resultConfig = buttonsConfig; + + if (buttonsLocation !== 'default') { + const position = splitPair(buttonsLocation); + + each(resultConfig, (_, element) => { + extend(element, { + toolbar: position[0], + location: position[1], + }); + }); + } - _setPopupOption: function(optionName, value) { - this._setWidgetOption('_popup', arguments); - }, + return resultConfig; + }, - _validatedOpening: function() { - if(!this.option('readOnly')) { - this._toggleOpenState(true); - } - }, - - _getPopupToolbarItems: function() { - return this.option('applyValueMode') === 'useButtons' - ? this._popupToolbarItemsConfig() - : []; - }, - - _getFirstPopupElement: function() { - return $(this._popup.getFocusableElements()).first(); - }, - - _getLastPopupElement: function() { - return $(this._popup.getFocusableElements()).last(); - }, - - _popupToolbarItemsConfig: function() { - const buttonsConfig = [ - { - shortcut: 'done', - options: { - onClick: this._applyButtonHandler.bind(this), - text: this.option('applyButtonText'), - } - }, - { - shortcut: 'cancel', - options: { - onClick: this._cancelButtonHandler.bind(this), - text: this.option('cancelButtonText'), - } - } - ]; - - return this._applyButtonsLocation(buttonsConfig); - }, - - _applyButtonsLocation: function(buttonsConfig) { - const buttonsLocation = this.option('buttonsLocation'); - const resultConfig = buttonsConfig; - - if(buttonsLocation !== 'default') { - const position = splitPair(buttonsLocation); - - each(resultConfig, function(_, element) { - extend(element, { - toolbar: position[0], - location: position[1] - }); - }); - } + _applyButtonHandler() { + this.close(); + this.option('focusStateEnabled') && this.focus(); + }, - return resultConfig; - }, + _cancelButtonHandler() { + this.close(); + this.option('focusStateEnabled') && this.focus(); + }, - _applyButtonHandler: function() { - this.close(); - this.option('focusStateEnabled') && this.focus(); - }, + _popupOptionChanged(args) { + // @ts-expect-error + const options = Widget.getOptionsFromContainer(args); - _cancelButtonHandler: function() { - this.close(); - this.option('focusStateEnabled') && this.focus(); - }, + this._setPopupOption(options); - _popupOptionChanged: function(args) { - const options = Widget.getOptionsFromContainer(args); + const optionsKeys = Object.keys(options); + if (optionsKeys.includes('width') || optionsKeys.includes('height')) { + this._dimensionChanged(); + } + }, - this._setPopupOption(options); + _renderSubmitElement() { + if (this.option('useHiddenSubmitElement')) { + this._$submitElement = $('') + .attr('type', 'hidden') + .appendTo(this.$element()); + } + }, - const optionsKeys = Object.keys(options); - if(optionsKeys.indexOf('width') !== -1 || optionsKeys.indexOf('height') !== -1) { - this._dimensionChanged(); - } - }, + _setSubmitValue() { + this._getSubmitElement().val(this.option('value')); + }, - _renderSubmitElement: function() { - if(this.option('useHiddenSubmitElement')) { - this._$submitElement = $('') - .attr('type', 'hidden') - .appendTo(this.$element()); + _getSubmitElement() { + if (this.option('useHiddenSubmitElement')) { + return this._$submitElement; + } + return this.callBase(); + }, + + _dispose() { + this._detachFocusOutEvents(); + this.callBase(); + }, + + _optionChanged(args) { + switch (args.name) { + case 'width': + case 'height': + this.callBase(args); + this._popup?.repaint(); + break; + case 'opened': + this._renderOpenedState(); + break; + case 'onOpened': + case 'onClosed': + this._initVisibilityActions(); + break; + case 'onPopupInitialized': // for dashboards + this._initPopupInitializedAction(); + break; + case 'fieldTemplate': + case 'acceptCustomValue': + case 'openOnFieldClick': + this._invalidate(); + break; + case 'dropDownButtonTemplate': + case 'showDropDownButton': + this._updateButtons(['dropDown']); + break; + case 'dropDownOptions': + this._popupOptionChanged(args); + this._options.cache('dropDownOptions', this.option('dropDownOptions')); + break; + case 'popupPosition': + break; + case 'deferRendering': + if (hasWindow()) { + this._createPopup(); + } + break; + case 'applyValueMode': + case 'applyButtonText': + case 'cancelButtonText': + case 'buttonsLocation': + this._setPopupOption('toolbarItems', this._getPopupToolbarItems()); + break; + case 'useHiddenSubmitElement': + if (this._$submitElement) { + this._$submitElement.remove(); + this._$submitElement = undefined; } - }, - _setSubmitValue: function() { - this._getSubmitElement().val(this.option('value')); - }, - - _getSubmitElement: function() { - if(this.option('useHiddenSubmitElement')) { - return this._$submitElement; - } else { - return this.callBase(); - } - }, - - _dispose: function() { - this._detachFocusOutEvents(); - this.callBase(); - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'width': - case 'height': - this.callBase(args); - this._popup?.repaint(); - break; - case 'opened': - this._renderOpenedState(); - break; - case 'onOpened': - case 'onClosed': - this._initVisibilityActions(); - break; - case 'onPopupInitialized': // for dashboards - this._initPopupInitializedAction(); - break; - case 'fieldTemplate': - case 'acceptCustomValue': - case 'openOnFieldClick': - this._invalidate(); - break; - case 'dropDownButtonTemplate': - case 'showDropDownButton': - this._updateButtons(['dropDown']); - break; - case 'dropDownOptions': - this._popupOptionChanged(args); - this._options.cache('dropDownOptions', this.option('dropDownOptions')); - break; - case 'popupPosition': - break; - case 'deferRendering': - if(hasWindow()) { - this._createPopup(); - } - break; - case 'applyValueMode': - case 'applyButtonText': - case 'cancelButtonText': - case 'buttonsLocation': - this._setPopupOption('toolbarItems', this._getPopupToolbarItems()); - break; - case 'useHiddenSubmitElement': - if(this._$submitElement) { - this._$submitElement.remove(); - this._$submitElement = undefined; - } - - this._renderSubmitElement(); - break; - case 'rtlEnabled': - this._updatePopupPosition(args.value); - this.callBase(args); - break; - default: - this.callBase(args); - } - }, + this._renderSubmitElement(); + break; + case 'rtlEnabled': + this._updatePopupPosition(args.value); + this.callBase(args); + break; + default: + this.callBase(args); + } + }, - open: function() { - this.option('opened', true); - }, + open() { + this.option('opened', true); + }, - close: function() { - this.option('opened', false); - }, + close() { + this.option('opened', false); + }, - field: function() { - return getPublicElement(this._input()); - }, + field() { + return getPublicElement(this._input()); + }, - content: function() { - return this._popup ? this._popup.content() : null; - } + content() { + return this._popup ? this._popup.content() : null; + }, }); registerComponent('dxDropDownEditor', DropDownEditor); diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts index 297675bc4054..629d9b4d78cc 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts @@ -1,25 +1,32 @@ -import { getOuterHeight } from '../../core/utils/size'; -import $ from '../../core/renderer'; -import { getWindow } from '../../core/utils/window'; +import registerComponent from '@js/core/component_registrator'; +import devices from '@js/core/devices'; +import Guid from '@js/core/guid'; +import $ from '@js/core/renderer'; +import { ChildDefaultTemplate } from '@js/core/templates/child_default_template'; +import { + ensureDefined, + // @ts-expect-error + grep, + noop, +} from '@js/core/utils/common'; +import { Deferred } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { getOuterHeight } from '@js/core/utils/size'; +import { isDefined, isObject, isWindow } from '@js/core/utils/type'; +import { getWindow } from '@js/core/utils/window'; +import dataQuery from '@js/data/query'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace } from '@js/events/utils/index'; +import messageLocalization from '@js/localization/message'; +import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; +import List from '@js/ui/list_light'; +import DataConverterMixin from '@js/ui/shared/grouped_data_converter_mixin'; +import errors from '@js/ui/widget/ui.errors'; + +import DropDownEditor from './m_drop_down_editor'; + const window = getWindow(); -import eventsEngine from '../../events/core/events_engine'; -import Guid from '../../core/guid'; -import registerComponent from '../../core/component_registrator'; -import { noop, ensureDefined, grep } from '../../core/utils/common'; -import { isWindow, isDefined, isObject } from '../../core/utils/type'; -import { extend } from '../../core/utils/extend'; -import DropDownEditor from './ui.drop_down_editor'; -import List from '../list_light'; -import errors from '../widget/ui.errors'; -import { addNamespace } from '../../events/utils/index'; -import devices from '../../core/devices'; -import dataQuery from '../../data/query'; -import { each } from '../../core/utils/iterator'; -import DataExpressionMixin from '../editor/ui.data_expression'; -import messageLocalization from '../../localization/message'; -import { ChildDefaultTemplate } from '../../core/templates/child_default_template'; -import { Deferred } from '../../core/utils/deferred'; -import DataConverterMixin from '../shared/grouped_data_converter_mixin'; const LIST_ITEM_SELECTOR = '.dx-list-item'; const LIST_ITEM_DATA_KEY = 'dxListItemData'; @@ -33,887 +40,869 @@ const useCompositionEvents = devices.real().platform !== 'android'; const DropDownList = DropDownEditor.inherit({ - _supportedKeys: function() { - const parent = this.callBase(); - - return extend({}, parent, { - tab: function(e) { - if(this._allowSelectItemByTab()) { - this._saveValueChangeEvent(e); - const $focusedItem = $(this._list.option('focusedElement')); - $focusedItem.length && this._setSelectedElement($focusedItem); - } - - parent.tab.apply(this, arguments); - }, - space: noop, - home: noop, - end: noop - }); - }, - - _allowSelectItemByTab: function() { - return this.option('opened') && this.option('applyValueMode') === 'instantly'; - }, - - _setSelectedElement: function($element) { - const value = this._valueGetter(this._list._getItemData($element)); - this._setValue(value); - }, - - _setValue: function(value) { - this.option('value', value); - }, - - _getDefaultOptions: function() { - return extend(this.callBase(), extend(DataExpressionMixin._dataExpressionDefaultOptions(), { - displayValue: undefined, - - searchEnabled: false, - - searchMode: 'contains', - - searchTimeout: 500, - - minSearchLength: 0, - - searchExpr: null, - - valueChangeEvent: 'input change keyup', - - selectedItem: null, - - noDataText: messageLocalization.format('dxCollectionWidget-noDataText'), - - encodeNoDataText: false, - - onSelectionChanged: null, - - onItemClick: noop, + _supportedKeys() { + const parent = this.callBase(); - showDataBeforeSearch: false, - - grouped: false, - - groupTemplate: 'group', - - popupPosition: { - my: 'left top', - at: 'left bottom', - offset: { h: 0, v: 0 }, - collision: 'flip' - }, - - wrapItemText: false, - - useItemTextAsTitle: false, - - /** - * @name dxDropDownListOptions.fieldTemplate - * @hidden - */ - - /** - * @name dxDropDownListOptions.applyValueMode - * @hidden - */ - })); - }, - - _defaultOptionsRules: function() { - return this.callBase().concat([ - { - device: { platform: 'ios' }, - options: { - popupPosition: { offset: { v: -1 } } - } - }, - { - device: { platform: 'generic' }, - options: { - buttonsLocation: 'bottom center' - } - } - ]); - }, - - _setOptionsByReference: function() { - this.callBase(); - - extend(this._optionsByReference, { - value: true, - selectedItem: true, - displayValue: true - }); - }, - - _init: function() { - this.callBase(); - - this._initDataExpressions(); - this._initActions(); - this._setListDataSource(); - this._validateSearchMode(); - this._clearSelectedItem(); - this._initItems(); - }, - - _setListFocusedElementOptionChange: function() { - this._list._updateParentActiveDescendant = this._updateActiveDescendant.bind(this); - }, - - _initItems: function() { - const items = this.option().items; - if(items && !items.length && this._dataSource) { - this.option().items = this._dataSource.items(); + return extend({}, parent, { + tab(e) { + if (this._allowSelectItemByTab()) { + this._saveValueChangeEvent(e); + const $focusedItem = $(this._list.option('focusedElement')); + $focusedItem.length && this._setSelectedElement($focusedItem); } - }, - _initActions: function() { - this._initContentReadyAction(); - this._initSelectionChangedAction(); - this._initItemClickAction(); - }, - - _initContentReadyAction: function() { - this._contentReadyAction = this._createActionByOption('onContentReady', { - excludeValidators: ['disabled', 'readOnly'] - }); - }, - - _initSelectionChangedAction: function() { - this._selectionChangedAction = this._createActionByOption('onSelectionChanged', { - excludeValidators: ['disabled', 'readOnly'] - }); - }, - - _initItemClickAction: function() { - this._itemClickAction = this._createActionByOption('onItemClick'); - }, - - _initTemplates: function() { - this.callBase(); - this._templateManager.addDefaultTemplates({ - item: new ChildDefaultTemplate('item') - }); - }, - - _isEditable: function() { - return this.callBase() || this.option('searchEnabled'); - }, - - _saveFocusOnWidget: function(e) { - if(this._list && this._list.initialOption('focusStateEnabled')) { - this._focusInput(); - } - }, - - _fitIntoRange: function(value, start, end) { - if(value > end) { - return start; - } - if(value < start) { - return end; - } - return value; - }, - - _items: function() { - const items = this._getPlainItems(!this._list && this._dataSource.items()); - - const availableItems = new dataQuery(items).filter('disabled', '<>', true).toArray(); - - return availableItems; - }, - - _calcNextItem: function(step) { - const items = this._items(); - const nextIndex = this._fitIntoRange(this._getSelectedIndex() + step, 0, items.length - 1); - return items[nextIndex]; - }, - - _getSelectedIndex: function() { - const items = this._items(); - const selectedItem = this.option('selectedItem'); - let result = -1; - each(items, (function(index, item) { - if(this._isValueEquals(item, selectedItem)) { - result = index; - return false; - } - }).bind(this)); - - return result; - }, - - _createPopup: function() { - this.callBase(); - this._updateCustomBoundaryContainer(); - this._popup.$wrapper().addClass(this._popupWrapperClass()); - - const $popupContent = this._popup.$content(); - eventsEngine.off($popupContent, 'mouseup'); - eventsEngine.on($popupContent, 'mouseup', this._saveFocusOnWidget.bind(this)); - }, - - _updateCustomBoundaryContainer: function() { - const customContainer = this.option('dropDownOptions.container'); - const $container = customContainer && $(customContainer); - - if($container && $container.length && !isWindow($container.get(0))) { - const $containerWithParents = [].slice.call($container.parents()); - $containerWithParents.unshift($container.get(0)); - - each($containerWithParents, function(i, parent) { - if(parent === $('body').get(0)) { - return false; - } else if(window.getComputedStyle(parent).overflowY === 'hidden') { - this._$customBoundaryContainer = $(parent); - return false; - } - }.bind(this)); - } - }, - - _popupWrapperClass: function() { - return DROPDOWNLIST_POPUP_WRAPPER_CLASS; - }, - - _renderInputValue: function() { - const value = this._getCurrentValue(); - this._rejectValueLoading(); - - return this._loadInputValue(value, this._setSelectedItem.bind(this)) - .always(this.callBase.bind(this, value)); - }, - - _loadInputValue: function(value, callback) { - return this._loadItem(value).always(callback); - }, - - _getItemFromPlain: function(value, cache) { - let plainItems; - let selectedItem; - - if(cache && typeof value !== 'object') { - if(!cache.itemByValue) { - cache.itemByValue = {}; - plainItems = this._getPlainItems(); - plainItems.forEach(function(item) { - cache.itemByValue[this._valueGetter(item)] = item; - }, this); - } - selectedItem = cache.itemByValue[value]; - } - - if(!selectedItem) { - plainItems = this._getPlainItems(); - selectedItem = grep(plainItems, (function(item) { - return this._isValueEquals(this._valueGetter(item), value); - }).bind(this))[0]; - } - - return selectedItem; - }, - - _loadItem: function(value, cache) { - const selectedItem = this._getItemFromPlain(value, cache); - - return selectedItem !== undefined - ? new Deferred().resolve(selectedItem).promise() - : this._loadValue(value); - }, - - _getPlainItems: function(items) { - let plainItems = []; - - items = items || this.option('items') || this._dataSource.items() || []; - - for(let i = 0; i < items.length; i++) { - if(items[i] && items[i].items) { - plainItems = plainItems.concat(items[i].items); - } else { - plainItems.push(items[i]); - } - } - - return plainItems; - }, - - _updateActiveDescendant($target) { - const opened = this.option('opened'); - const listFocusedItemId = this._list?.getFocusedItemId(); - const isElementOnDom = $(`#${listFocusedItemId}`).length > 0; - const activedescendant = opened && isElementOnDom && listFocusedItemId; - - this.setAria({ - 'activedescendant': activedescendant || null - }, $target); - }, - - _setSelectedItem: function(item) { - const displayValue = this._displayValue(item); - this.option('selectedItem', ensureDefined(item, null)); - this.option('displayValue', displayValue); - }, - - _displayValue: function(item) { - return this._displayGetter(item); - }, - - _refreshSelected: function() { - const cache = {}; - this._listItemElements().each((function(_, itemElement) { - const $itemElement = $(itemElement); - const itemValue = this._valueGetter($itemElement.data(LIST_ITEM_DATA_KEY)); - - const isItemSelected = this._isSelectedValue(itemValue, cache); - - if(isItemSelected) { - this._list.selectItem($itemElement); - } else { - this._list.unselectItem($itemElement); - } - }).bind(this)); - }, - - _popupShownHandler: function() { - this.callBase(); - this._setFocusPolicy(); - }, - - _setFocusPolicy: function() { - if(!this.option('focusStateEnabled') || !this._list) { - return; - } - - this._list.option('focusedElement', null); - }, - - _isSelectedValue: function(value) { - return this._isValueEquals(value, this.option('value')); - }, - - _validateSearchMode: function() { - const searchMode = this.option('searchMode'); - const normalizedSearchMode = searchMode.toLowerCase(); + parent.tab.apply(this, arguments); + }, + space: noop, + home: noop, + end: noop, + }); + }, + + _allowSelectItemByTab() { + return this.option('opened') && this.option('applyValueMode') === 'instantly'; + }, + + _setSelectedElement($element) { + const value = this._valueGetter(this._list._getItemData($element)); + this._setValue(value); + }, + + _setValue(value) { + this.option('value', value); + }, + + _getDefaultOptions() { + // @ts-expect-error + return extend(this.callBase(), extend(DataExpressionMixin._dataExpressionDefaultOptions(), { + displayValue: undefined, + searchEnabled: false, + searchMode: 'contains', + searchTimeout: 500, + minSearchLength: 0, + searchExpr: null, + valueChangeEvent: 'input change keyup', + selectedItem: null, + noDataText: messageLocalization.format('dxCollectionWidget-noDataText'), + encodeNoDataText: false, + onSelectionChanged: null, + onItemClick: noop, + showDataBeforeSearch: false, + grouped: false, + groupTemplate: 'group', + popupPosition: { + my: 'left top', + at: 'left bottom', + offset: { h: 0, v: 0 }, + collision: 'flip', + }, + wrapItemText: false, + useItemTextAsTitle: false, + })); + }, + + _defaultOptionsRules() { + return this.callBase().concat([ + { + device: { platform: 'ios' }, + options: { + popupPosition: { offset: { v: -1 } }, + }, + }, + { + device: { platform: 'generic' }, + options: { + buttonsLocation: 'bottom center', + }, + }, + ]); + }, + + _setOptionsByReference() { + this.callBase(); + + extend(this._optionsByReference, { + value: true, + selectedItem: true, + displayValue: true, + }); + }, + + _init() { + this.callBase(); + + this._initDataExpressions(); + this._initActions(); + this._setListDataSource(); + this._validateSearchMode(); + this._clearSelectedItem(); + this._initItems(); + }, + + _setListFocusedElementOptionChange() { + this._list._updateParentActiveDescendant = this._updateActiveDescendant.bind(this); + }, + + _initItems() { + const { items } = this.option(); + if (items && !items.length && this._dataSource) { + this.option().items = this._dataSource.items(); + } + }, + + _initActions() { + this._initContentReadyAction(); + this._initSelectionChangedAction(); + this._initItemClickAction(); + }, + + _initContentReadyAction() { + this._contentReadyAction = this._createActionByOption('onContentReady', { + excludeValidators: ['disabled', 'readOnly'], + }); + }, + + _initSelectionChangedAction() { + this._selectionChangedAction = this._createActionByOption('onSelectionChanged', { + excludeValidators: ['disabled', 'readOnly'], + }); + }, + + _initItemClickAction() { + this._itemClickAction = this._createActionByOption('onItemClick'); + }, + + _initTemplates() { + this.callBase(); + this._templateManager.addDefaultTemplates({ + item: new ChildDefaultTemplate('item'), + }); + }, + + _isEditable() { + return this.callBase() || this.option('searchEnabled'); + }, + + _saveFocusOnWidget() { + if (this._list && this._list.initialOption('focusStateEnabled')) { + this._focusInput(); + } + }, - if(!SEARCH_MODES.includes(normalizedSearchMode)) { - throw errors.Error('E1019', searchMode); + _fitIntoRange(value, start, end) { + if (value > end) { + return start; + } + if (value < start) { + return end; + } + return value; + }, + + _items() { + const items = this._getPlainItems(!this._list && this._dataSource.items()); + // @ts-expect-error + // eslint-disable-next-line new-cap + const availableItems = new dataQuery(items).filter('disabled', '<>', true).toArray(); + + return availableItems; + }, + + _calcNextItem(step) { + const items = this._items(); + const nextIndex = this._fitIntoRange(this._getSelectedIndex() + step, 0, items.length - 1); + return items[nextIndex]; + }, + + _getSelectedIndex() { + const items = this._items(); + const selectedItem = this.option('selectedItem'); + let result = -1; + // @ts-expect-error + each(items, (index, item) => { + if (this._isValueEquals(item, selectedItem)) { + // @ts-expect-error + result = index; + return false; + } + }); + + return result; + }, + + _createPopup() { + this.callBase(); + this._updateCustomBoundaryContainer(); + this._popup.$wrapper().addClass(this._popupWrapperClass()); + + const $popupContent = this._popup.$content(); + eventsEngine.off($popupContent, 'mouseup'); + eventsEngine.on($popupContent, 'mouseup', this._saveFocusOnWidget.bind(this)); + }, + + _updateCustomBoundaryContainer() { + const customContainer = this.option('dropDownOptions.container'); + const $container = customContainer && $(customContainer); + + if ($container && $container.length && !isWindow($container.get(0))) { + const $containerWithParents = [].slice.call($container.parents()); + // @ts-expect-error + $containerWithParents.unshift($container.get(0)); + + // @ts-expect-error + each($containerWithParents, (i, parent) => { + if (parent === $('body').get(0)) { + return false; + } if (window.getComputedStyle(parent).overflowY === 'hidden') { + this._$customBoundaryContainer = $(parent); + return false; } - }, - - _clearSelectedItem: function() { - this.option('selectedItem', null); - }, - - _processDataSourceChanging: function() { - this._initDataController(); - this._setListOption('_dataController', this._dataController); - this._setListDataSource(); - - this._renderInputValue().fail((function() { - if(this._isCustomValueAllowed()) { - return; - } - this._clearSelectedItem(); - }).bind(this)); - }, - - _isCustomValueAllowed: function() { - return this.option('displayCustomValue'); - }, - - clear: function() { - this.callBase(); - - this._clearFilter(); - this._clearSelectedItem(); - }, - - _listItemElements: function() { - return this._$list ? this._$list.find(LIST_ITEM_SELECTOR) : $(); - }, - - _popupConfig: function() { - return extend(this.callBase(), { - templatesRenderAsynchronously: false, - autoResizeEnabled: false, - maxHeight: this._getMaxHeight.bind(this), - }); - }, - - _renderPopupContent: function() { - this.callBase(); - this._renderList(); - }, - - _getKeyboardListeners() { - const canListHaveFocus = this._canListHaveFocus(); - - return this.callBase().concat([!canListHaveFocus && this._list]); - }, - - _renderList: function() { - this._listId = 'dx-' + new Guid()._value; - - const $list = $('
') - .attr('id', this._listId) - .appendTo(this._popup.$content()); - this._$list = $list; - - this._list = this._createComponent($list, List, this._listConfig()); - this._refreshList(); - - this._renderPreventBlurOnListClick(); - this._setListFocusedElementOptionChange(); - }, - - _renderPreventBlurOnListClick: function() { - const eventName = addNamespace('mousedown', 'dxDropDownList'); + }); + } + }, + + _popupWrapperClass() { + return DROPDOWNLIST_POPUP_WRAPPER_CLASS; + }, + + _renderInputValue() { + const value = this._getCurrentValue(); + this._rejectValueLoading(); + + return this._loadInputValue(value, this._setSelectedItem.bind(this)) + .always(this.callBase.bind(this, value)); + }, + + _loadInputValue(value, callback) { + return this._loadItem(value).always(callback); + }, + + _getItemFromPlain(value, cache) { + let plainItems; + let selectedItem; + + if (cache && typeof value !== 'object') { + if (!cache.itemByValue) { + cache.itemByValue = {}; + plainItems = this._getPlainItems(); + plainItems.forEach(function (item) { + cache.itemByValue[this._valueGetter(item)] = item; + }, this); + } + selectedItem = cache.itemByValue[value]; + } - eventsEngine.off(this._$list, eventName); - eventsEngine.on(this._$list, eventName, (e) => e.preventDefault()); - }, + if (!selectedItem) { + plainItems = this._getPlainItems(); + // eslint-disable-next-line prefer-destructuring + selectedItem = grep(plainItems, (item) => this._isValueEquals(this._valueGetter(item), value))[0]; + } - _getControlsAria() { - return this._list && this._listId; - }, + return selectedItem; + }, - _renderOpenedState: function() { - this.callBase(); + _loadItem(value, cache) { + const selectedItem = this._getItemFromPlain(value, cache); - this._list && this._updateActiveDescendant(); - this.setAria('owns', this._popup && this._popupContentId); - }, + return selectedItem !== undefined + ? Deferred().resolve(selectedItem).promise() + : this._loadValue(value); + }, - _getAriaHasPopup() { - return 'listbox'; - }, + _getPlainItems(items) { + let plainItems: any = []; - _refreshList: function() { - if(this._list && this._shouldRefreshDataSource()) { - this._setListDataSource(); - } - }, - - _shouldRefreshDataSource: function() { - const dataSourceProvided = !!this._list.option('dataSource'); - - return dataSourceProvided !== this._needPassDataSourceToList(); - }, - - _isDesktopDevice: function() { - return devices.real().deviceType === 'desktop'; - }, - - _listConfig: function() { - const options = { - selectionMode: 'single', - _templates: this.option('_templates'), - templateProvider: this.option('templateProvider'), - noDataText: this.option('noDataText'), - encodeNoDataText: this.option('encodeNoDataText'), - grouped: this.option('grouped'), - wrapItemText: this.option('wrapItemText'), - useItemTextAsTitle: this.option('useItemTextAsTitle'), - onContentReady: this._listContentReadyHandler.bind(this), - itemTemplate: this.option('itemTemplate'), - indicateLoading: false, - keyExpr: this._getCollectionKeyExpr(), - displayExpr: this._displayGetterExpr(), - groupTemplate: this.option('groupTemplate'), - onItemClick: this._listItemClickAction.bind(this), - dataSource: this._getDataSource(), - _dataController: this._dataController, - hoverStateEnabled: this._isDesktopDevice() ? this.option('hoverStateEnabled') : false, - focusStateEnabled: this._isDesktopDevice() ? this.option('focusStateEnabled') : false - }; - - if(!this._canListHaveFocus()) { - options.tabIndex = null; - } + items = items || this.option('items') || this._dataSource.items() || []; - return options; - }, - - _canListHaveFocus: () => false, - - _getDataSource: function() { - return this._needPassDataSourceToList() ? this._dataSource : null; - }, + for (let i = 0; i < items.length; i++) { + if (items[i] && items[i].items) { + plainItems = plainItems.concat(items[i].items); + } else { + plainItems.push(items[i]); + } + } - _dataSourceOptions: function() { - return { - paginate: false - }; - }, + return plainItems; + }, + + _updateActiveDescendant($target) { + const opened = this.option('opened'); + const listFocusedItemId = this._list?.getFocusedItemId(); + const isElementOnDom = $(`#${listFocusedItemId}`).length > 0; + const activedescendant = opened && isElementOnDom && listFocusedItemId; + + this.setAria({ + activedescendant: activedescendant || null, + }, $target); + }, + + _setSelectedItem(item) { + const displayValue = this._displayValue(item); + this.option('selectedItem', ensureDefined(item, null)); + this.option('displayValue', displayValue); + }, + + _displayValue(item) { + return this._displayGetter(item); + }, + + _refreshSelected() { + const cache = {}; + this._listItemElements().each((_, itemElement) => { + const $itemElement = $(itemElement); + const itemValue = this._valueGetter($itemElement.data(LIST_ITEM_DATA_KEY)); + + const isItemSelected = this._isSelectedValue(itemValue, cache); + + if (isItemSelected) { + this._list.selectItem($itemElement); + } else { + this._list.unselectItem($itemElement); + } + }); + }, + + _popupShownHandler() { + this.callBase(); + this._setFocusPolicy(); + }, + + _setFocusPolicy() { + if (!this.option('focusStateEnabled') || !this._list) { + return; + } - _getGroupedOption: function() { - return this.option('grouped'); - }, + this._list.option('focusedElement', null); + }, - _dataSourceFromUrlLoadMode: function() { - return 'raw'; - }, + _isSelectedValue(value) { + return this._isValueEquals(value, this.option('value')); + }, - _listContentReadyHandler: function() { - this._list = this._list || this._$list.dxList('instance'); + _validateSearchMode() { + const searchMode = this.option('searchMode'); + const normalizedSearchMode = searchMode.toLowerCase(); - if(!this.option('deferRendering')) { - this._refreshSelected(); - } + if (!SEARCH_MODES.includes(normalizedSearchMode)) { + throw errors.Error('E1019', searchMode); + } + }, + + _clearSelectedItem() { + this.option('selectedItem', null); + }, + + _processDataSourceChanging() { + this._initDataController(); + this._setListOption('_dataController', this._dataController); + this._setListDataSource(); + + this._renderInputValue().fail(() => { + if (this._isCustomValueAllowed()) { + return; + } + this._clearSelectedItem(); + }); + }, + + _isCustomValueAllowed() { + return this.option('displayCustomValue'); + }, + + clear() { + this.callBase(); + + this._clearFilter(); + this._clearSelectedItem(); + }, + + _listItemElements() { + // @ts-expect-error + return this._$list ? this._$list.find(LIST_ITEM_SELECTOR) : $(); + }, + + _popupConfig() { + return extend(this.callBase(), { + templatesRenderAsynchronously: false, + autoResizeEnabled: false, + maxHeight: this._getMaxHeight.bind(this), + }); + }, + + _renderPopupContent() { + this.callBase(); + this._renderList(); + }, + + _getKeyboardListeners() { + const canListHaveFocus = this._canListHaveFocus(); + + return this.callBase().concat([!canListHaveFocus && this._list]); + }, + + _renderList() { + // @ts-expect-error + this._listId = `dx-${new Guid()._value}`; + + const $list = $('
') + .attr('id', this._listId) + .appendTo(this._popup.$content()); + this._$list = $list; + + this._list = this._createComponent($list, List, this._listConfig()); + this._refreshList(); + + this._renderPreventBlurOnListClick(); + this._setListFocusedElementOptionChange(); + }, + + _renderPreventBlurOnListClick() { + const eventName = addNamespace('mousedown', 'dxDropDownList'); + + eventsEngine.off(this._$list, eventName); + eventsEngine.on(this._$list, eventName, (e) => e.preventDefault()); + }, + + _getControlsAria() { + return this._list && this._listId; + }, + + _renderOpenedState() { + this.callBase(); + + this._list && this._updateActiveDescendant(); + this.setAria('owns', this._popup && this._popupContentId); + }, + + _getAriaHasPopup() { + return 'listbox'; + }, + + _refreshList() { + if (this._list && this._shouldRefreshDataSource()) { + this._setListDataSource(); + } + }, + + _shouldRefreshDataSource() { + const dataSourceProvided = !!this._list.option('dataSource'); + + return dataSourceProvided !== this._needPassDataSourceToList(); + }, + + _isDesktopDevice() { + return devices.real().deviceType === 'desktop'; + }, + + _listConfig() { + const options = { + selectionMode: 'single', + _templates: this.option('_templates'), + templateProvider: this.option('templateProvider'), + noDataText: this.option('noDataText'), + encodeNoDataText: this.option('encodeNoDataText'), + grouped: this.option('grouped'), + wrapItemText: this.option('wrapItemText'), + useItemTextAsTitle: this.option('useItemTextAsTitle'), + onContentReady: this._listContentReadyHandler.bind(this), + itemTemplate: this.option('itemTemplate'), + indicateLoading: false, + keyExpr: this._getCollectionKeyExpr(), + displayExpr: this._displayGetterExpr(), + groupTemplate: this.option('groupTemplate'), + onItemClick: this._listItemClickAction.bind(this), + dataSource: this._getDataSource(), + _dataController: this._dataController, + hoverStateEnabled: this._isDesktopDevice() ? this.option('hoverStateEnabled') : false, + focusStateEnabled: this._isDesktopDevice() ? this.option('focusStateEnabled') : false, + }; + + if (!this._canListHaveFocus()) { + // @ts-expect-error + options.tabIndex = null; + } - this._updatePopupWidth(); - this._updateListDimensions(); + return options; + }, - this._contentReadyAction(); - }, + _canListHaveFocus: () => false, - _setListOption: function(optionName, value) { - this._setWidgetOption('_list', arguments); - }, + _getDataSource() { + return this._needPassDataSourceToList() ? this._dataSource : null; + }, - _listItemClickAction: function(e) { - this._listItemClickHandler(e); - this._itemClickAction(e); - }, + _dataSourceOptions() { + return { + paginate: false, + }; + }, - _listItemClickHandler: noop, + _getGroupedOption() { + return this.option('grouped'); + }, - _setListDataSource: function() { - if(!this._list) { - return; - } + _dataSourceFromUrlLoadMode() { + return 'raw'; + }, - this._setListOption('dataSource', this._getDataSource()); + _listContentReadyHandler() { + this._list = this._list || this._$list.dxList('instance'); - if(!this._needPassDataSourceToList()) { - this._setListOption('items', []); - } - }, - - _needPassDataSourceToList: function() { - return this.option('showDataBeforeSearch') || this._isMinSearchLengthExceeded(); - }, - - _isMinSearchLengthExceeded: function() { - return this._searchValue().toString().length >= this.option('minSearchLength'); - }, - - _needClearFilter: function() { - return this._canKeepDataSource() ? false : this._needPassDataSourceToList(); - }, - - _canKeepDataSource: function() { - const isMinSearchLengthExceeded = this._isMinSearchLengthExceeded(); - return this._dataController.isLoaded() && - this.option('showDataBeforeSearch') && - this.option('minSearchLength') && - !isMinSearchLengthExceeded && - !this._isLastMinSearchLengthExceeded; - }, - - _searchValue: function() { - return this._input().val() || ''; - }, - - _getSearchEvent: function() { - return addNamespace(SEARCH_EVENT, this.NAME + 'Search'); - }, - - _getCompositionStartEvent: function() { - return addNamespace('compositionstart', this.NAME + 'CompositionStart'); - }, - - _getCompositionEndEvent: function() { - return addNamespace('compositionend', this.NAME + 'CompositionEnd'); - }, - - _getSetFocusPolicyEvent: function() { - return addNamespace('input', this.NAME + 'FocusPolicy'); - }, - - _renderEvents: function() { - this.callBase(); - eventsEngine.on(this._input(), this._getSetFocusPolicyEvent(), () => { this._setFocusPolicy(); }); - - if(this._shouldRenderSearchEvent()) { - eventsEngine.on(this._input(), this._getSearchEvent(), (e) => { this._searchHandler(e); }); - if(useCompositionEvents) { - eventsEngine.on(this._input(), this._getCompositionStartEvent(), () => { this._isTextCompositionInProgress(true); }); - eventsEngine.on(this._input(), this._getCompositionEndEvent(), (e) => { - this._isTextCompositionInProgress(undefined); - this._searchHandler(e, this._searchValue()); - }); - } - } - }, - - _shouldRenderSearchEvent: function() { - return this.option('searchEnabled'); - }, - - _refreshEvents: function() { - eventsEngine.off(this._input(), this._getSearchEvent()); - eventsEngine.off(this._input(), this._getSetFocusPolicyEvent()); - if(useCompositionEvents) { - eventsEngine.off(this._input(), this._getCompositionStartEvent()); - eventsEngine.off(this._input(), this._getCompositionEndEvent()); - } + if (!this.option('deferRendering')) { + this._refreshSelected(); + } - this.callBase(); - }, + this._updatePopupWidth(); + this._updateListDimensions(); - _isTextCompositionInProgress: function(value) { - if(arguments.length) { - this._isTextComposition = value; - } else { - return this._isTextComposition; - } - }, + this._contentReadyAction(); + }, - _searchHandler: function(e, searchValue) { - if(this._isTextCompositionInProgress()) { - return; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _setListOption(optionName, value) { + this._setWidgetOption('_list', arguments); + }, - if(!this._isMinSearchLengthExceeded()) { - this._searchCanceled(); - return; - } + _listItemClickAction(e) { + this._listItemClickHandler(e); + this._itemClickAction(e); + }, - const searchTimeout = this.option('searchTimeout'); + _listItemClickHandler: noop, - if(searchTimeout) { - this._clearSearchTimer(); - this._searchTimer = setTimeout( - () => { this._searchDataSource(searchValue); }, - searchTimeout - ); - } else { - this._searchDataSource(searchValue); - } - }, + _setListDataSource() { + if (!this._list) { + return; + } - _searchCanceled: function() { - this._clearSearchTimer(); - if(this._needClearFilter()) { - this._filterDataSource(null); - } - this._refreshList(); - }, + this._setListOption('dataSource', this._getDataSource()); - _searchDataSource: function(searchValue = this._searchValue()) { - this._filterDataSource(searchValue); - }, + if (!this._needPassDataSourceToList()) { + this._setListOption('items', []); + } + }, + + _needPassDataSourceToList() { + return this.option('showDataBeforeSearch') || this._isMinSearchLengthExceeded(); + }, + + _isMinSearchLengthExceeded() { + return this._searchValue().toString().length >= this.option('minSearchLength'); + }, + + _needClearFilter() { + return this._canKeepDataSource() ? false : this._needPassDataSourceToList(); + }, + + _canKeepDataSource() { + const isMinSearchLengthExceeded = this._isMinSearchLengthExceeded(); + return this._dataController.isLoaded() + && this.option('showDataBeforeSearch') + && this.option('minSearchLength') + && !isMinSearchLengthExceeded + && !this._isLastMinSearchLengthExceeded; + }, + + _searchValue() { + return this._input().val() || ''; + }, + + _getSearchEvent() { + return addNamespace(SEARCH_EVENT, `${this.NAME}Search`); + }, + + _getCompositionStartEvent() { + return addNamespace('compositionstart', `${this.NAME}CompositionStart`); + }, + + _getCompositionEndEvent() { + return addNamespace('compositionend', `${this.NAME}CompositionEnd`); + }, + + _getSetFocusPolicyEvent() { + return addNamespace('input', `${this.NAME}FocusPolicy`); + }, + + _renderEvents() { + this.callBase(); + eventsEngine.on(this._input(), this._getSetFocusPolicyEvent(), () => { this._setFocusPolicy(); }); + + if (this._shouldRenderSearchEvent()) { + eventsEngine.on(this._input(), this._getSearchEvent(), (e) => { this._searchHandler(e); }); + if (useCompositionEvents) { + eventsEngine.on(this._input(), this._getCompositionStartEvent(), () => { this._isTextCompositionInProgress(true); }); + eventsEngine.on(this._input(), this._getCompositionEndEvent(), (e) => { + this._isTextCompositionInProgress(undefined); + this._searchHandler(e, this._searchValue()); + }); + } + } + }, + + _shouldRenderSearchEvent() { + return this.option('searchEnabled'); + }, + + _refreshEvents() { + eventsEngine.off(this._input(), this._getSearchEvent()); + eventsEngine.off(this._input(), this._getSetFocusPolicyEvent()); + if (useCompositionEvents) { + eventsEngine.off(this._input(), this._getCompositionStartEvent()); + eventsEngine.off(this._input(), this._getCompositionEndEvent()); + } - _filterDataSource: function(searchValue) { - this._clearSearchTimer(); + this.callBase(); + }, - const dataController = this._dataController; + _isTextCompositionInProgress(value) { + if (arguments.length) { + this._isTextComposition = value; + } else { + return this._isTextComposition; + } + }, - dataController.searchExpr(this.option('searchExpr') || this._displayGetterExpr()); - dataController.searchOperation(this.option('searchMode')); - dataController.searchValue(searchValue); - dataController.load().done(this._dataSourceFiltered.bind(this, searchValue)); - }, + _searchHandler(e, searchValue) { + if (this._isTextCompositionInProgress()) { + return; + } - _clearFilter: function() { - const dataController = this._dataController; - dataController.searchValue() && dataController.searchValue(null); - }, + if (!this._isMinSearchLengthExceeded()) { + this._searchCanceled(); + return; + } - _dataSourceFiltered: function() { - this._isLastMinSearchLengthExceeded = this._isMinSearchLengthExceeded(); - this._refreshList(); - this._refreshPopupVisibility(); - }, + const searchTimeout = this.option('searchTimeout'); - _shouldOpenPopup: function() { - return this._hasItemsToShow(); - }, + if (searchTimeout) { + this._clearSearchTimer(); + this._searchTimer = setTimeout( + () => { this._searchDataSource(searchValue); }, + searchTimeout, + ); + } else { + this._searchDataSource(searchValue); + } + }, - _refreshPopupVisibility: function() { - if(this.option('readOnly') || !this._searchValue()) { - return; - } + _searchCanceled() { + this._clearSearchTimer(); + if (this._needClearFilter()) { + this._filterDataSource(null); + } + this._refreshList(); + }, + + _searchDataSource(searchValue = this._searchValue()) { + this._filterDataSource(searchValue); + }, + + _filterDataSource(searchValue) { + this._clearSearchTimer(); + + const dataController = this._dataController; + + dataController.searchExpr(this.option('searchExpr') || this._displayGetterExpr()); + dataController.searchOperation(this.option('searchMode')); + dataController.searchValue(searchValue); + dataController.load().done(this._dataSourceFiltered.bind(this, searchValue)); + }, + + _clearFilter() { + const dataController = this._dataController; + dataController.searchValue() && dataController.searchValue(null); + }, + + _dataSourceFiltered() { + this._isLastMinSearchLengthExceeded = this._isMinSearchLengthExceeded(); + this._refreshList(); + this._refreshPopupVisibility(); + }, + + _shouldOpenPopup() { + return this._hasItemsToShow(); + }, + + _refreshPopupVisibility() { + if (this.option('readOnly') || !this._searchValue()) { + return; + } - const shouldOpenPopup = this._shouldOpenPopup(); + const shouldOpenPopup = this._shouldOpenPopup(); - if(shouldOpenPopup && !this._isFocused()) { - return; - } + if (shouldOpenPopup && !this._isFocused()) { + return; + } - this.option('opened', shouldOpenPopup); + this.option('opened', shouldOpenPopup); - if(shouldOpenPopup) { - this._updatePopupWidth(); - this._updateListDimensions(); - } - }, + if (shouldOpenPopup) { + this._updatePopupWidth(); + this._updateListDimensions(); + } + }, - _dataSourceChangedHandler: function(newItems) { - if(this._dataController.pageIndex() === 0) { - this.option().items = newItems; - } else { - this.option().items = this.option().items.concat(newItems); - } - }, + _dataSourceChangedHandler(newItems) { + if (this._dataController.pageIndex() === 0) { + this.option().items = newItems; + } else { + this.option().items = this.option().items.concat(newItems); + } + }, - _hasItemsToShow: function() { - const dataController = this._dataController; - const resultItems = dataController.items() || []; - const resultAmount = resultItems.length; - const isMinSearchLengthExceeded = this._needPassDataSourceToList(); + _hasItemsToShow() { + const dataController = this._dataController; + const resultItems = dataController.items() || []; + const resultAmount = resultItems.length; + const isMinSearchLengthExceeded = this._needPassDataSourceToList(); - return !!(isMinSearchLengthExceeded && resultAmount); - }, + return !!(isMinSearchLengthExceeded && resultAmount); + }, - _clearSearchTimer: function() { - clearTimeout(this._searchTimer); - delete this._searchTimer; - }, + _clearSearchTimer() { + clearTimeout(this._searchTimer); + delete this._searchTimer; + }, - _popupShowingHandler: function() { - this._updatePopupWidth(); - this._updateListDimensions(); - }, + _popupShowingHandler() { + this._updatePopupWidth(); + this._updateListDimensions(); + }, - _dimensionChanged: function() { - this.callBase(); + _dimensionChanged() { + this.callBase(); - this._updateListDimensions(); - }, + this._updateListDimensions(); + }, - _needPopupRepaint: function() { - const dataController = this._dataController; - const currentPageIndex = dataController.pageIndex(); - const needRepaint = (isDefined(this._pageIndex) && currentPageIndex <= this._pageIndex) || (dataController.isLastPage() && !this._list._scrollViewIsFull()); + _needPopupRepaint() { + const dataController = this._dataController; + const currentPageIndex = dataController.pageIndex(); + const needRepaint = (isDefined(this._pageIndex) && currentPageIndex <= this._pageIndex) || (dataController.isLastPage() && !this._list._scrollViewIsFull()); - this._pageIndex = currentPageIndex; + this._pageIndex = currentPageIndex; - return needRepaint; - }, + return needRepaint; + }, - _updateListDimensions: function() { - if(!this._popup) { - return; - } + _updateListDimensions() { + if (!this._popup) { + return; + } - if(this._needPopupRepaint()) { - this._popup.repaint(); - } + if (this._needPopupRepaint()) { + this._popup.repaint(); + } - this._list && this._list.updateDimensions(); - }, + this._list && this._list.updateDimensions(); + }, - _getMaxHeight: function() { - const $element = this.$element(); - const $customBoundaryContainer = this._$customBoundaryContainer; - const offsetTop = $element.offset().top - ($customBoundaryContainer ? $customBoundaryContainer.offset().top : 0); - const windowHeight = getOuterHeight(window); - const containerHeight = $customBoundaryContainer ? Math.min(getOuterHeight($customBoundaryContainer), windowHeight) : windowHeight; - const maxHeight = Math.max(offsetTop, containerHeight - offsetTop - getOuterHeight($element)); + _getMaxHeight() { + const $element = this.$element(); + const $customBoundaryContainer = this._$customBoundaryContainer; + const offsetTop = $element.offset().top - ($customBoundaryContainer ? $customBoundaryContainer.offset().top : 0); + const windowHeight = getOuterHeight(window); + const containerHeight = $customBoundaryContainer ? Math.min(getOuterHeight($customBoundaryContainer), windowHeight) : windowHeight; + const maxHeight = Math.max(offsetTop, containerHeight - offsetTop - getOuterHeight($element)); - return Math.min(containerHeight * 0.5, maxHeight); - }, + return Math.min(containerHeight * 0.5, maxHeight); + }, - _clean: function() { - if(this._list) { - delete this._list; + _clean() { + if (this._list) { + delete this._list; + } + delete this._isLastMinSearchLengthExceeded; + this.callBase(); + }, + + _dispose() { + this._clearSearchTimer(); + this.callBase(); + }, + + _setCollectionWidgetOption() { + this._setListOption.apply(this, arguments); + }, + + _setSubmitValue() { + const value = this.option('value'); + const submitValue = this._shouldUseDisplayValue(value) ? this._displayGetter(value) : value; + + this._getSubmitElement().val(submitValue); + }, + + _shouldUseDisplayValue(value) { + return this.option('valueExpr') === 'this' && isObject(value); + }, + + _optionChanged(args) { + this._dataExpressionOptionChanged(args); + switch (args.name) { + case 'hoverStateEnabled': + case 'focusStateEnabled': + this._isDesktopDevice() && this._setListOption(args.name, args.value); + this.callBase(args); + break; + case 'items': + if (!this.option('dataSource')) { + this._processDataSourceChanging(); } - delete this._isLastMinSearchLengthExceeded; - this.callBase(); - }, - - _dispose: function() { - this._clearSearchTimer(); - this.callBase(); - }, - - _setCollectionWidgetOption: function() { - this._setListOption.apply(this, arguments); - }, - - _setSubmitValue: function() { - const value = this.option('value'); - const submitValue = this._shouldUseDisplayValue(value) ? this._displayGetter(value) : value; - - this._getSubmitElement().val(submitValue); - }, - - _shouldUseDisplayValue: function(value) { - return this.option('valueExpr') === 'this' && isObject(value); - }, - - _optionChanged: function(args) { - this._dataExpressionOptionChanged(args); - switch(args.name) { - case 'hoverStateEnabled': - case 'focusStateEnabled': - this._isDesktopDevice() && this._setListOption(args.name, args.value); - this.callBase(args); - break; - case 'items': - if(!this.option('dataSource')) { - this._processDataSourceChanging(); - } - break; - case 'dataSource': - this._processDataSourceChanging(); - break; - case 'valueExpr': - this._renderValue(); - this._setListOption('keyExpr', this._getCollectionKeyExpr()); - break; - case 'displayExpr': - this._renderValue(); - this._setListOption('displayExpr', this._displayGetterExpr()); - break; - case 'searchMode': - this._validateSearchMode(); - break; - case 'minSearchLength': - this._refreshList(); - break; - case 'searchEnabled': - case 'showDataBeforeSearch': - case 'searchExpr': - this._invalidate(); - break; - case 'onContentReady': - this._initContentReadyAction(); - break; - case 'onSelectionChanged': - this._initSelectionChangedAction(); - break; - case 'onItemClick': - this._initItemClickAction(); - break; - case 'grouped': - case 'groupTemplate': - case 'wrapItemText': - case 'noDataText': - case 'encodeNoDataText': - case 'useItemTextAsTitle': - this._setListOption(args.name); - break; - case 'displayValue': - this.option('text', args.value); - break; - case 'itemTemplate': - case 'searchTimeout': - break; - case 'selectedItem': - if(args.previousValue !== args.value) { - this._selectionChangedAction({ selectedItem: args.value }); - } - break; - default: - this.callBase(args); + break; + case 'dataSource': + this._processDataSourceChanging(); + break; + case 'valueExpr': + this._renderValue(); + this._setListOption('keyExpr', this._getCollectionKeyExpr()); + break; + case 'displayExpr': + this._renderValue(); + this._setListOption('displayExpr', this._displayGetterExpr()); + break; + case 'searchMode': + this._validateSearchMode(); + break; + case 'minSearchLength': + this._refreshList(); + break; + case 'searchEnabled': + case 'showDataBeforeSearch': + case 'searchExpr': + this._invalidate(); + break; + case 'onContentReady': + this._initContentReadyAction(); + break; + case 'onSelectionChanged': + this._initSelectionChangedAction(); + break; + case 'onItemClick': + this._initItemClickAction(); + break; + case 'grouped': + case 'groupTemplate': + case 'wrapItemText': + case 'noDataText': + case 'encodeNoDataText': + case 'useItemTextAsTitle': + this._setListOption(args.name); + break; + case 'displayValue': + this.option('text', args.value); + break; + case 'itemTemplate': + case 'searchTimeout': + break; + case 'selectedItem': + if (args.previousValue !== args.value) { + this._selectionChangedAction({ selectedItem: args.value }); } + break; + default: + this.callBase(args); } + }, }).include(DataExpressionMixin, DataConverterMixin); diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts index 50aff9eed62c..576fd0c47a2e 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts @@ -1,21 +1,22 @@ -import { getOuterWidth } from '../../core/utils/size'; -import { hasWindow } from '../../core/utils/window'; +import type { dxElementWrapper } from '@js/core/renderer'; +import { getOuterWidth } from '@js/core/utils/size'; +import { hasWindow } from '@js/core/utils/window'; -const getElementWidth = function($element) { - if(hasWindow()) { - return getOuterWidth($element); - } +const getElementWidth = function ($element: dxElementWrapper) { + if (hasWindow()) { + return getOuterWidth($element); + } }; -const getSizeValue = function(size) { - if(size === null) { - size = undefined; - } - if(typeof size === 'function') { - size = size(); - } +const getSizeValue = function (size) { + if (size === null) { + size = undefined; + } + if (typeof size === 'function') { + size = size(); + } - return size; + return size; }; export { getElementWidth, getSizeValue }; diff --git a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts index fa7f5c0d77e7..114e0c0131b4 100644 --- a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts +++ b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts @@ -11,9 +11,9 @@ import { map } from '@js/core/utils/iterator'; import { isDefined, isObject } from '@js/core/utils/type'; import eventsEngine from '@js/events/core/events_engine'; import { normalizeKeyName } from '@js/events/utils/index'; -import DropDownEditor from '@js/ui/drop_down_editor/ui.drop_down_editor'; import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; import { tabbable } from '@js/ui/widget/selectors'; +import DropDownEditor from '@ts/ui/drop_down_editor/m_drop_down_editor'; import { getElementMaxHeightByWindow } from '@ts/ui/overlay/m_utils'; const { getActiveElement } = domAdapter; diff --git a/packages/devextreme/js/__internal/ui/m_drop_down_button.ts b/packages/devextreme/js/__internal/ui/m_drop_down_button.ts index d81a2c4ba65c..be9bc4afaf67 100644 --- a/packages/devextreme/js/__internal/ui/m_drop_down_button.ts +++ b/packages/devextreme/js/__internal/ui/m_drop_down_button.ts @@ -12,10 +12,10 @@ import { isDefined, isPlainObject } from '@js/core/utils/type'; import DataController from '@js/data_controller'; import messageLocalization from '@js/localization/message'; import ButtonGroup from '@js/ui/button_group'; -import { getElementWidth, getSizeValue } from '@js/ui/drop_down_editor/utils'; import List from '@js/ui/list_light'; import Popup from '@js/ui/popup/ui.popup'; import Widget from '@js/ui/widget/ui.widget'; +import { getElementWidth, getSizeValue } from '@ts/ui/drop_down_editor/m_utils'; const DROP_DOWN_BUTTON_CLASS = 'dx-dropdownbutton'; const DROP_DOWN_BUTTON_CONTENT = 'dx-dropdownbutton-content'; diff --git a/packages/devextreme/js/__internal/ui/m_lookup.ts b/packages/devextreme/js/__internal/ui/m_lookup.ts index bdb81b3353ae..7079b955b6a2 100644 --- a/packages/devextreme/js/__internal/ui/m_lookup.ts +++ b/packages/devextreme/js/__internal/ui/m_lookup.ts @@ -17,10 +17,10 @@ import { getWindow } from '@js/core/utils/window'; import eventsEngine from '@js/events/core/events_engine'; import messageLocalization from '@js/localization/message'; import DropDownList from '@js/ui/drop_down_editor/ui.drop_down_list'; -import { getElementWidth } from '@js/ui/drop_down_editor/utils'; import Popover from '@js/ui/popover/ui.popover'; import TextBox from '@js/ui/text_box'; import { current, isMaterial } from '@js/ui/themes'; +import { getElementWidth } from '@ts/ui/drop_down_editor/m_utils'; const window = getWindow(); diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js new file mode 100644 index 000000000000..cfbba249bbae --- /dev/null +++ b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js @@ -0,0 +1,38 @@ +import DropDownEditor from '../../__internal/ui/drop_down_editor/m_drop_down_editor'; + +export default DropDownEditor; + +/** + * @name dxDropDownEditorOptions.mask + * @hidden + */ + +/** + * @name dxDropDownEditorOptions.maskChar + * @hidden + */ + +/** + * @name dxDropDownEditorOptions.maskRules + * @hidden + */ + +/** + * @name dxDropDownEditorOptions.maskInvalidMessage + * @hidden + */ + +/** + * @name dxDropDownEditorOptions.useMaskedValue + * @hidden + */ + +/** + * @name dxDropDownEditorOptions.mode + * @hidden + */ + +/** + * @name dxDropDownEditorOptions.showMaskMode + * @hidden + */ diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js new file mode 100644 index 000000000000..80d5eef1dc20 --- /dev/null +++ b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js @@ -0,0 +1,13 @@ +import DropDownList from '../../__internal/ui/drop_down_editor/m_drop_down_list'; + +export default DropDownList; + +/** + * @name dxDropDownListOptions.fieldTemplate + * @hidden + */ + +/** + * @name dxDropDownListOptions.applyValueMode + * @hidden + */ diff --git a/packages/devextreme/testing/helpers/widgetsList.js b/packages/devextreme/testing/helpers/widgetsList.js index c46e92083895..a589b178ae13 100644 --- a/packages/devextreme/testing/helpers/widgetsList.js +++ b/packages/devextreme/testing/helpers/widgetsList.js @@ -80,8 +80,8 @@ const dropDownEditorsList = { dxDropDownButton: require('ui/drop_down_button'), dxSelectBox: require('ui/select_box'), dxTagBox: require('ui/tag_box'), - dxDropDownEditor: require('ui/drop_down_editor/ui.drop_down_editor.js'), - dxDropDownList: require('ui/drop_down_editor/ui.drop_down_list.js'), + dxDropDownEditor: require('ui/drop_down_editor/ui.drop_down_editor'), + dxDropDownList: require('ui/drop_down_editor/ui.drop_down_list'), }; exports.widgetsList = widgetsList; From 70344925d089f458c99f164973b6dbd1fa4d7542 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 11 Jun 2024 12:23:48 +0400 Subject: [PATCH 18/32] FloatingActionButton, SpeeDialAction: move files to TS --- .../ui/speed_dial_action/m_repaint_floating_action_button.ts} | 0 .../ui/speed_dial_action/m_speed_dial_action.ts} | 0 .../ui/speed_dial_action/m_speed_dial_item.ts} | 0 .../ui/speed_dial_action/m_speed_dial_main_item.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/speed_dial_action/repaint_floating_action_button.js => __internal/ui/speed_dial_action/m_repaint_floating_action_button.ts} (100%) rename packages/devextreme/js/{ui/speed_dial_action/speed_dial_action.js => __internal/ui/speed_dial_action/m_speed_dial_action.ts} (100%) rename packages/devextreme/js/{ui/speed_dial_action/speed_dial_item.js => __internal/ui/speed_dial_action/m_speed_dial_item.ts} (100%) rename packages/devextreme/js/{ui/speed_dial_action/speed_dial_main_item.js => __internal/ui/speed_dial_action/m_speed_dial_main_item.ts} (100%) diff --git a/packages/devextreme/js/ui/speed_dial_action/repaint_floating_action_button.js b/packages/devextreme/js/__internal/ui/speed_dial_action/m_repaint_floating_action_button.ts similarity index 100% rename from packages/devextreme/js/ui/speed_dial_action/repaint_floating_action_button.js rename to packages/devextreme/js/__internal/ui/speed_dial_action/m_repaint_floating_action_button.ts diff --git a/packages/devextreme/js/ui/speed_dial_action/speed_dial_action.js b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts similarity index 100% rename from packages/devextreme/js/ui/speed_dial_action/speed_dial_action.js rename to packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts diff --git a/packages/devextreme/js/ui/speed_dial_action/speed_dial_item.js b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts similarity index 100% rename from packages/devextreme/js/ui/speed_dial_action/speed_dial_item.js rename to packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts diff --git a/packages/devextreme/js/ui/speed_dial_action/speed_dial_main_item.js b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts similarity index 100% rename from packages/devextreme/js/ui/speed_dial_action/speed_dial_main_item.js rename to packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts From 094f88a22e4c1326509868583e237c0ef1a00d52 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 11 Jun 2024 14:31:48 +0400 Subject: [PATCH 19/32] FloatingActionButton, SpeeDialAction: ignore errors after move to TS --- .../grid_core/validating/m_validating.ts | 1 + .../js/__internal/ui/m_load_panel.ts | 2 +- .../js/__internal/ui/overlay/m_overlay.ts | 7 +- .../m_repaint_floating_action_button.ts | 3 +- .../speed_dial_action/m_speed_dial_action.ts | 186 ++-- .../ui/speed_dial_action/m_speed_dial_item.ts | 435 +++++---- .../m_speed_dial_main_item.ts | 885 +++++++++--------- .../js/__internal/ui/toast/m_toast.ts | 2 +- .../devextreme/js/ui/speed_dial_action.js | 20 +- .../repaint_floating_action_button.js | 3 + .../speedDialAction.tests.js | 2 +- 11 files changed, 816 insertions(+), 730 deletions(-) create mode 100644 packages/devextreme/js/ui/speed_dial_action/repaint_floating_action_button.js diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts index e64513911e97..bf1a9be3550b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts @@ -1251,6 +1251,7 @@ export const validatingEditorFactoryExtender = (Base: ModuleType) this._hideFixedGroupCell($cell, overlayOptions); + // @ts-expect-error // eslint-disable-next-line no-new new Overlay($overlayElement, overlayOptions); } diff --git a/packages/devextreme/js/__internal/ui/m_load_panel.ts b/packages/devextreme/js/__internal/ui/m_load_panel.ts index a64519ffc3c9..814d508493e6 100644 --- a/packages/devextreme/js/__internal/ui/m_load_panel.ts +++ b/packages/devextreme/js/__internal/ui/m_load_panel.ts @@ -15,7 +15,7 @@ const LOADPANEL_MESSAGE_CLASS = 'dx-loadpanel-message'; const LOADPANEL_CONTENT_CLASS = 'dx-loadpanel-content'; const LOADPANEL_CONTENT_WRAPPER_CLASS = 'dx-loadpanel-content-wrapper'; const LOADPANEL_PANE_HIDDEN_CLASS = 'dx-loadpanel-pane-hidden'; - +// @ts-expect-error const LoadPanel = Overlay.inherit({ _supportedKeys() { return extend(this.callBase(), { diff --git a/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts b/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts index 57bd56c97295..d806aa1bb7f8 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts +++ b/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts @@ -28,6 +28,7 @@ import { keyboard } from '@js/events/short'; import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '@js/events/utils/index'; import { triggerHidingEvent, triggerResizeEvent, triggerShownEvent } from '@js/events/visibility_change'; import { hideCallback as hideTopOverlayCallback } from '@js/mobile/hide_callback'; +import type OverlayInstance from '@js/ui/overlay'; import { tabbable } from '@js/ui/widget/selectors'; import uiErrors from '@js/ui/widget/ui.errors'; import Widget from '@js/ui/widget/ui.widget'; @@ -67,8 +68,9 @@ ready(() => { } }); }); + // @ts-expect-error -const Overlay = Widget.inherit({ +const Overlay: typeof OverlayInstance = Widget.inherit({ _supportedKeys() { return extend(this.callBase(), { escape() { @@ -348,6 +350,7 @@ const Overlay = Widget.inherit({ }, _zIndexInitValue() { + // @ts-expect-error return Overlay.baseZIndex(); }, @@ -1289,7 +1292,7 @@ const Overlay = Widget.inherit({ } }, }); - +// @ts-expect-error Overlay.baseZIndex = (zIndex) => zIndexPool.base(zIndex); registerComponent('dxOverlay', Overlay); diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_repaint_floating_action_button.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_repaint_floating_action_button.ts index 63b5a7bd723e..973f9b1079cc 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_repaint_floating_action_button.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_repaint_floating_action_button.ts @@ -1,4 +1,3 @@ -import { repaint } from './speed_dial_main_item'; - +import { repaint } from './m_speed_dial_main_item'; export default repaint; diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts index 8d6a7515083e..1ec17ff2ebdb 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts @@ -1,116 +1,98 @@ -import registerComponent from '../../core/component_registrator'; -import { extend } from '../../core/utils/extend'; -import Guid from '../../core/guid'; -import readyCallbacks from '../../core/utils/ready_callbacks'; -import Widget from '../widget/ui.widget'; -import { initAction, disposeAction } from './speed_dial_main_item'; -import swatchContainer from '../widget/swatch_container'; +import registerComponent from '@js/core/component_registrator'; +import Guid from '@js/core/guid'; +import { extend } from '@js/core/utils/extend'; +import readyCallbacks from '@js/core/utils/ready_callbacks'; +import type { Properties } from '@js/ui/speed_dial_action'; +import swatchContainer from '@js/ui/widget/swatch_container'; +import Widget from '@ts/ui/widget'; -const { getSwatchContainer } = swatchContainer; +import { disposeAction, initAction } from './m_speed_dial_main_item'; -// STYLE speedDialAction +const { getSwatchContainer } = swatchContainer; const ready = readyCallbacks.add; -class SpeedDialAction extends Widget { - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - icon: '', - - - onClick: null, - - label: '', - - visible: true, - - index: 0, - - /** - * @name dxSpeedDialActionOptions.width - * @hidden - */ - - /** - * @name dxSpeedDialActionOptions.height - * @hidden - */ - - /** - * @name dxSpeedDialActionOptions.disabled - * @hidden - */ - - - onContentReady: null, - - activeStateEnabled: true, - hoverStateEnabled: true, - animation: { - show: { - type: 'pop', - duration: 200, - easing: 'cubic-bezier(0.4, 0, 0.2, 1)', - from: { - scale: 0, - opacity: 0 - }, - to: { - scale: 1, - opacity: 1 - } - }, - hide: { - type: 'pop', - duration: 200, - easing: 'cubic-bezier(0.4, 0, 0.2, 1)', - from: { - scale: 1, - opacity: 1 - }, - to: { - scale: 0, - opacity: 0 - } - } - }, - id: new Guid() - }); - } - _optionChanged(args) { - switch(args.name) { - case 'onClick': - case 'icon': - case 'label': - case 'visible': - case 'index': - case 'onInitializing': - initAction(this); - break; - case 'animation': - case 'id': - break; - default: - super._optionChanged(args); - } +class SpeedDialAction extends Widget { + _getDefaultOptions() { + return extend(super._getDefaultOptions(), { + icon: '', + onClick: null, + label: '', + visible: true, + index: 0, + onContentReady: null, + activeStateEnabled: true, + hoverStateEnabled: true, + animation: { + show: { + type: 'pop', + duration: 200, + easing: 'cubic-bezier(0.4, 0, 0.2, 1)', + from: { + scale: 0, + opacity: 0, + }, + to: { + scale: 1, + opacity: 1, + }, + }, + hide: { + type: 'pop', + duration: 200, + easing: 'cubic-bezier(0.4, 0, 0.2, 1)', + from: { + scale: 1, + opacity: 1, + }, + to: { + scale: 0, + opacity: 0, + }, + }, + }, + id: new Guid(), + }); + } + + _optionChanged(args) { + switch (args.name) { + case 'onClick': + case 'icon': + case 'label': + case 'visible': + case 'index': + case 'onInitializing': + initAction(this); + break; + case 'animation': + case 'id': + break; + default: + super._optionChanged(args); } + } - _render() { - this._toggleVisibility(false); + _render(): void { + // @ts-expect-error + this._toggleVisibility(false); - if(!getSwatchContainer(this.$element())) { - ready(() => initAction(this)); - } else { - initAction(this); - } - } - _dispose() { - disposeAction(this._options.silent('id')); - super._dispose(); + if (!getSwatchContainer(this.$element())) { + ready(() => initAction(this)); + } else { + initAction(this); } + } + + _dispose() { + // @ts-expect-error + disposeAction(this._options.silent('id')); + // @ts-expect-error + super._dispose(); + } } +// @ts-expect-error registerComponent('dxSpeedDialAction', SpeedDialAction); export default SpeedDialAction; - diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts index 62a8578faa58..cc43aca58d0f 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts @@ -1,13 +1,15 @@ -import $ from '../../core/renderer'; -import { extend } from '../../core/utils/extend'; -import eventsEngine from '../../events/core/events_engine'; -import { addNamespace } from '../../events/utils/index'; -import { name as clickEventName } from '../../events/click'; -import { getImageContainer } from '../../core/utils/icon'; -import Overlay from '../overlay/ui.overlay'; -import { render } from '../widget/utils.ink_ripple'; -import { isMaterial } from '../themes'; -import { isPlainObject } from '../../core/utils/type'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { getImageContainer } from '@js/core/utils/icon'; +import { isPlainObject } from '@js/core/utils/type'; +import { name as clickEventName } from '@js/events/click'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace } from '@js/events/utils/index'; +import Overlay from '@js/ui/overlay/ui.overlay'; +import type { Properties as PublicProperties } from '@js/ui/speed_dial_action'; +import { isMaterial } from '@js/ui/themes'; +import { render } from '@js/ui/widget/utils.ink_ripple'; const FAB_CLASS = 'dx-fa-button'; const FAB_ICON_CLASS = 'dx-fa-button-icon'; @@ -16,221 +18,262 @@ const FAB_LABEL_WRAPPER_CLASS = 'dx-fa-button-label-wrapper'; const FAB_CONTENT_REVERSE_CLASS = 'dx-fa-button-content-reverse'; const OVERLAY_CONTENT_SELECTOR = '.dx-overlay-content'; -class SpeedDialItem extends Overlay { - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - shading: false, - useInkRipple: false, - callOverlayRenderShading: false, - width: 'auto', - zIndex: 1500, - _observeContentResize: false, - }); - } - - _defaultOptionsRules() { - return super._defaultOptionsRules().concat([ - { - device() { - return isMaterial(); - }, - options: { - useInkRipple: true - } - } - ]); - } - - _moveToContainer() { - this._$wrapper.appendTo(this.$element()); - this._$content.appendTo(this._$wrapper); - } - - _render() { - this.$element().addClass(FAB_CLASS); - this._renderIcon(); - this._renderLabel(); - super._render(); - this.option('useInkRipple') && this._renderInkRipple(); - this._renderClick(); - } - - _renderLabel() { - !!this._$label && this._$label.remove(); - - const labelText = this.option('label'); - - if(!labelText) { - this._$label = null; - return; - } - - const $element = $('
').addClass(FAB_LABEL_CLASS); - const $wrapper = $('
').addClass(FAB_LABEL_WRAPPER_CLASS); - - this._$label = $wrapper - .prependTo(this.$content()) - .append($element.text(labelText)); - - this.$content().toggleClass(FAB_CONTENT_REVERSE_CLASS, this._isPositionLeft(this.option('parentPosition'))); - } - - _isPositionLeft(position) { - let currentLocation = ''; - - if(position) { - if(isPlainObject(position) && position.at) { - if(position.at.x) { - currentLocation = position.at.x; - } else { - currentLocation = position.at; - } - } else { - if(typeof position === 'string') { - currentLocation = position; - } - } - } - - return currentLocation.split(' ')[0] === 'left'; - } - - _renderButtonIcon($element, icon, iconClass) { - !!$element && $element.remove(); +export interface Properties extends PublicProperties { + zIndex: number; - $element = $('
').addClass(iconClass); - const $iconElement = getImageContainer(icon); - - $element - .append($iconElement) - .appendTo(this.$content()); + actions?: any; +} - return $element; +class SpeedDialItem extends Overlay { + // Temporary solution. Move to component level + public NAME!: string; + + _$label?: dxElementWrapper; + + _currentVisible?: boolean; + + _$wrapper!: dxElementWrapper; + + _$content!: dxElementWrapper; + + _inkRipple?: any; + + _$icon?: dxElementWrapper; + + _options?: any; + + _contentReadyAction?: any; + + _clickAction?: any; + + _getDefaultOptions() { + // @ts-expect-error + return extend(super._getDefaultOptions(), { + shading: false, + useInkRipple: false, + callOverlayRenderShading: false, + width: 'auto', + zIndex: 1500, + _observeContentResize: false, + }); + } + + _defaultOptionsRules() { + // @ts-expect-error + return super._defaultOptionsRules().concat([ + { + device() { + // @ts-expect-error + return isMaterial(); + }, + options: { + useInkRipple: true, + }, + }, + ]); + } + + _moveToContainer() { + this._$wrapper.appendTo(this.$element()); + this._$content.appendTo(this._$wrapper); + } + + _render(): void { + // @ts-expect-error + this.$element().addClass(FAB_CLASS); + this._renderIcon(); + this._renderLabel(); + // @ts-expect-error + super._render(); + this.option('useInkRipple') && this._renderInkRipple(); + this._renderClick(); + } + + _renderLabel() { + !!this._$label && this._$label.remove(); + + const labelText = this.option('label'); + + if (!labelText) { + // @ts-expect-error + this._$label = null; + return; } - _renderIcon() { - this._$icon = this._renderButtonIcon( - this._$icon, - this._options.silent('icon'), - FAB_ICON_CLASS); - } + const $element = $('
').addClass(FAB_LABEL_CLASS); + const $wrapper = $('
').addClass(FAB_LABEL_WRAPPER_CLASS); - _renderWrapper() { - if(this._options.silent('callOverlayRenderShading')) { - super._renderWrapper(); - } - } + this._$label = $wrapper + // @ts-expect-error + .prependTo(this.$content()) + .append($element.text(labelText)); - _getVisibleActions(actions) { - const currentActions = actions || this.option('actions') || []; + // @ts-expect-error + this.$content().toggleClass(FAB_CONTENT_REVERSE_CLASS, this._isPositionLeft(this.option('parentPosition'))); + } - return currentActions.filter((action) => action.option('visible')); - } + _isPositionLeft(position) { + let currentLocation = ''; - _getActionComponent() { - if(this._getVisibleActions().length === 1) { - return this._getVisibleActions()[0]; + if (position) { + if (isPlainObject(position) && position.at) { + if (position.at.x) { + currentLocation = position.at.x; } else { - return this.option('actionComponent') || this.option('actions')[0]; + currentLocation = position.at; } + } else if (typeof position === 'string') { + currentLocation = position; + } } - _initContentReadyAction() { - this._contentReadyAction = this._getActionComponent()._createActionByOption('onContentReady', { - excludeValidators: ['disabled', 'readOnly'] - }, true); - } + return currentLocation.split(' ')[0] === 'left'; + } - _fireContentReadyAction() { - this._contentReadyAction({ actionElement: this.$element() }); - } + _renderButtonIcon($element, icon, iconClass) { + !!$element && $element.remove(); - _updateZIndexStackPosition() { - const zIndex = this.option('zIndex'); + $element = $('
').addClass(iconClass); + const $iconElement = getImageContainer(icon); - this._$wrapper.css('zIndex', zIndex); - this._$content.css('zIndex', zIndex); - } + $element + .append($iconElement) + // @ts-expect-error + .appendTo(this.$content()); + return $element; + } - _setClickAction() { - const eventName = addNamespace(clickEventName, this.NAME); - const overlayContent = this.$element().find(OVERLAY_CONTENT_SELECTOR); + _renderIcon() { + this._$icon = this._renderButtonIcon( + this._$icon, + this._options.silent('icon'), + FAB_ICON_CLASS, + ); + } - eventsEngine.off(overlayContent, eventName); - eventsEngine.on(overlayContent, eventName, (e) => { - const clickActionArgs = { - event: e, - actionElement: this.element(), - element: this._getActionComponent().$element() - }; - - this._clickAction(clickActionArgs); - }); + _renderWrapper() { + if (this._options.silent('callOverlayRenderShading')) { + // @ts-expect-error + super._renderWrapper(); } + } - _defaultActionArgs() { - return { - component: this._getActionComponent() - }; - } + _getVisibleActions(actions) { + const currentActions = actions || this.option('actions') || []; - _renderClick() { - this._clickAction = this._getActionComponent()._createActionByOption('onClick'); - this._setClickAction(); - } + return currentActions.filter((action) => action.option('visible')); + } - _renderInkRipple() { - this._inkRipple = render(); + _getActionComponent() { + // @ts-expect-error + if (this._getVisibleActions().length === 1) { + // @ts-expect-error + return this._getVisibleActions()[0]; } - - _getInkRippleContainer() { - return this._$icon; + return this.option('actionComponent') || this.option('actions')[0]; + } + + _initContentReadyAction() { + this._contentReadyAction = this._getActionComponent()._createActionByOption('onContentReady', { + excludeValidators: ['disabled', 'readOnly'], + }, true); + } + + _fireContentReadyAction() { + this._contentReadyAction({ actionElement: this.$element() }); + } + + _updateZIndexStackPosition() { + const zIndex = this.option('zIndex'); + + this._$wrapper.css('zIndex', zIndex); + this._$content.css('zIndex', zIndex); + } + + _setClickAction(): void { + const eventName = addNamespace(clickEventName, this.NAME); + // @ts-expect-error + const overlayContent = this.$element().find(OVERLAY_CONTENT_SELECTOR); + + eventsEngine.off(overlayContent, eventName); + eventsEngine.on(overlayContent, eventName, (e) => { + const clickActionArgs = { + event: e, + actionElement: this.element(), + element: this._getActionComponent().$element(), + }; + + this._clickAction(clickActionArgs); + }); + } + + _defaultActionArgs() { + return { + component: this._getActionComponent(), + }; + } + + _renderClick() { + this._clickAction = this._getActionComponent()._createActionByOption('onClick'); + this._setClickAction(); + } + + _renderInkRipple() { + this._inkRipple = render(); + } + + _getInkRippleContainer() { + return this._$icon; + } + + _toggleActiveState($element, value, e) { + // @ts-expect-error + super._toggleActiveState.apply(this, arguments); + + if (!this._inkRipple) { + return; } - _toggleActiveState($element, value, e) { - super._toggleActiveState.apply(this, arguments); - - if(!this._inkRipple) { - return; - } - - const config = { - element: this._getInkRippleContainer(), - event: e - }; + const config = { + element: this._getInkRippleContainer(), + event: e, + }; - if(value) { - this._inkRipple.showWave(config); - } else { - this._inkRipple.hideWave(config); - } + if (value) { + this._inkRipple.showWave(config); + } else { + this._inkRipple.hideWave(config); } + } - _optionChanged(args) { - switch(args.name) { - case 'icon': - this._renderIcon(); - break; - case 'onClick': - this._renderClick(); - break; - case 'label': - this._renderLabel(); - break; - case 'visible': - this._currentVisible = args.previousValue; - args.value ? - this._show() : - this._hide(); - break; - case 'useInkRipple': - this._render(); - break; - default: - super._optionChanged(args); - } + _optionChanged(args) { + switch (args.name) { + case 'icon': + this._renderIcon(); + break; + case 'onClick': + this._renderClick(); + break; + case 'label': + this._renderLabel(); + break; + case 'visible': + this._currentVisible = args.previousValue; + args.value + // @ts-expect-error + ? this._show() + // @ts-expect-error + : this._hide(); + break; + case 'useInkRipple': + this._render(); + break; + default: + // @ts-expect-error + super._optionChanged(args); } + } } export default SpeedDialItem; diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts index 83478d3a6d03..15c48196802b 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts @@ -1,12 +1,15 @@ -import { getHeight } from '../../core/utils/size'; -import $ from '../../core/renderer'; -import config from '../../core/config'; -import { extend } from '../../core/utils/extend'; -import eventsEngine from '../../events/core/events_engine'; -import errors from '../widget/ui.errors'; -import swatchContainer from '../widget/swatch_container'; -import SpeedDialItem from './speed_dial_item'; -import { isCompact, isMaterial, isFluent } from '../themes'; +import config from '@js/core/config'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { getHeight } from '@js/core/utils/size'; +import eventsEngine from '@js/events/core/events_engine'; +import { isCompact, isFluent, isMaterial } from '@js/ui/themes'; +import swatchContainer from '@js/ui/widget/swatch_container'; +import errors from '@js/ui/widget/ui.errors'; + +import type { Properties as SpeedDialItemProperties } from './m_speed_dial_item'; +import SpeedDialItem from './m_speed_dial_item'; const { getSwatchContainer } = swatchContainer; @@ -16,469 +19,503 @@ const FAB_MAIN_CLASS_WITHOUT_ICON = 'dx-fa-button-without-icon'; const FAB_CLOSE_ICON_CLASS = 'dx-fa-button-icon-close'; const INVISIBLE_STATE_CLASS = 'dx-state-invisible'; -let speedDialMainItem = null; +let speedDialMainItem: SpeedDialMainItem | null = null; const modifyActionOptions = (action) => { - const { - animation, - actionComponent, - actionVisible, - actions, - activeStateEnabled, - direction, - elementAttr, - hint, - hoverStateEnabled, - icon, - id, - index, - label, - onClick, - onContentReady, - parentPosition, - position, - visible, - zIndex, - } = action.option(); - - return extend({}, { - animation, - actionComponent, - actionVisible, - actions, - activeStateEnabled, - direction, - elementAttr, - hint, - hoverStateEnabled, - icon, - id, - index, - label, - onClick, - onContentReady, - parentPosition, - position, - visible, - zIndex, - }, { - onInitialized: null, - onDisposing: null - }); + const { + animation, + actionComponent, + actionVisible, + actions, + activeStateEnabled, + direction, + elementAttr, + hint, + hoverStateEnabled, + icon, + id, + index, + label, + onClick, + onContentReady, + parentPosition, + position, + visible, + zIndex, + } = action.option(); + + return extend({}, { + animation, + actionComponent, + actionVisible, + actions, + activeStateEnabled, + direction, + elementAttr, + hint, + hoverStateEnabled, + icon, + id, + index, + label, + onClick, + onContentReady, + parentPosition, + position, + visible, + zIndex, + }, { + onInitialized: null, + onDisposing: null, + }); }; -class SpeedDialMainItem extends SpeedDialItem { - _getDefaultOptions() { - const defaultOptions = { - icon: 'add', - closeIcon: 'close', - position: { - at: 'right bottom', - my: 'right bottom', - offset: { - x: -16, - y: -16 - } - }, - maxSpeedDialActionCount: 5, - hint: '', - label: '', - direction: 'auto', - actions: [], - activeStateEnabled: true, - hoverStateEnabled: true, - indent: isCompact() ? 49 : 55, - childIndent: 40, - childOffset: isCompact() ? 2 : 9, - callOverlayRenderShading: true, - hideOnOutsideClick: true - }; - - return extend( - super._getDefaultOptions(), - extend(defaultOptions, config().floatingActionButtonConfig, { shading: false }) - ); - } - - _defaultOptionsRules() { - return super._defaultOptionsRules().concat([ - { - device() { - return isFluent() && !isCompact(); - }, - options: { - indent: 60, - childIndent: 60, - childOffset: 0 - } - }, - { - device() { - return isFluent() && isCompact(); - }, - options: { - indent: 48, - childIndent: 48, - childOffset: 0 - } - }, - { - device() { - return isMaterial() && !isCompact(); - }, - options: { - indent: 72, - childIndent: 56, - childOffset: 8 - } - }, - { - device() { - return isMaterial() && isCompact(); - }, - options: { - indent: 58, - childIndent: 48, - childOffset: 1 - } - } - ]); - } +export interface Properties extends SpeedDialItemProperties { + maxSpeedDialActionCount: number; +} - _render() { - this.$element().addClass(FAB_MAIN_CLASS); - super._render(); - this._moveToContainer(); - this._renderCloseIcon(); - this._renderClick(); +class SpeedDialMainItem extends SpeedDialItem { + _isShadingShown?: boolean; + + _$closeIcon!: dxElementWrapper; + + _$icon!: dxElementWrapper; + + _actionItems?: any; + + _getDefaultOptions() { + const defaultOptions = { + icon: 'add', + closeIcon: 'close', + position: { + at: 'right bottom', + my: 'right bottom', + offset: { + x: -16, + y: -16, + }, + }, + maxSpeedDialActionCount: 5, + hint: '', + label: '', + direction: 'auto', + actions: [], + activeStateEnabled: true, + hoverStateEnabled: true, + // @ts-expect-error + indent: isCompact() ? 49 : 55, + childIndent: 40, + // @ts-expect-error + childOffset: isCompact() ? 2 : 9, + callOverlayRenderShading: true, + hideOnOutsideClick: true, + }; + + return extend( + super._getDefaultOptions(), + extend(defaultOptions, config().floatingActionButtonConfig, { shading: false }), + ); + } + + _defaultOptionsRules() { + return super._defaultOptionsRules().concat([ + { + device() { + // @ts-expect-error + return isFluent() && !isCompact(); + }, + options: { + indent: 60, + childIndent: 60, + childOffset: 0, + }, + }, + { + device() { + // @ts-expect-error + return isFluent() && isCompact(); + }, + options: { + indent: 48, + childIndent: 48, + childOffset: 0, + }, + }, + { + device() { + // @ts-expect-error + return isMaterial() && !isCompact(); + }, + options: { + indent: 72, + childIndent: 56, + childOffset: 8, + }, + }, + { + device() { + // @ts-expect-error + return isMaterial() && isCompact(); + }, + options: { + indent: 58, + childIndent: 48, + childOffset: 1, + }, + }, + ]); + } + + _render() { + // @ts-expect-error + this.$element().addClass(FAB_MAIN_CLASS); + super._render(); + this._moveToContainer(); + this._renderCloseIcon(); + this._renderClick(); + } + + _renderLabel() { + super._renderLabel(); + // @ts-expect-error + this.$element().toggleClass(FAB_MAIN_CLASS_WITH_LABEL, !!this._$label); + } + + _renderIcon() { + super._renderIcon(); + // @ts-expect-error + this.$element().toggleClass(FAB_MAIN_CLASS_WITHOUT_ICON, !this.option('icon')); + } + + _renderCloseIcon() { + this._$closeIcon = this._renderButtonIcon( + this._$closeIcon, + this._options.silent('closeIcon'), + FAB_CLOSE_ICON_CLASS, + ); + + this._$closeIcon.addClass(INVISIBLE_STATE_CLASS); + } + + _renderClick() { + this._clickAction = this._getVisibleActions().length === 1 + ? this._getActionComponent()._createActionByOption('onClick') + // @ts-expect-error + : this._createAction(this._clickHandler.bind(this)); + + this._setClickAction(); + } + + _getVisibleActions(actions?: any) { + const currentActions = actions || this.option('actions'); + + return currentActions.filter((action) => action.option('visible')); + } + + _getCurrentOptions(actions) { + const visibleActions = speedDialMainItem?._getVisibleActions(actions); + + const defaultOptions = this._getDefaultOptions(); + + delete defaultOptions.closeOnOutsideClick; + + return visibleActions.length === 1 + ? extend(modifyActionOptions(visibleActions[0]), { position: this._getPosition() }) + : extend(defaultOptions, { visible: visibleActions.length !== 0 }); + } + + _clickHandler(): void { + const actions = this._actionItems + .filter((action) => action.option('actionVisible')) + .sort((action, nextAction) => action.option('index') - nextAction.option('index')); + + if (actions.length === 1) return; + + const lastActionIndex = actions.length - 1; + + for (let i = 0; i < actions.length; i++) { + actions[i].option('animation', this._getActionAnimation(actions[i], i, lastActionIndex)); + actions[i].option('position', this._getActionPosition(actions, i)); + // @ts-expect-error + actions[i]._$wrapper.css('position', this._$wrapper.css('position')); + actions[i].toggle(); } - _renderLabel() { - super._renderLabel(); - this.$element().toggleClass(FAB_MAIN_CLASS_WITH_LABEL, !!this._$label); + if (config().floatingActionButtonConfig?.shading) { + this._isShadingShown = !this.option('shading'); + this.option('shading', this._isShadingShown); } - _renderIcon() { - super._renderIcon(); + this._$icon.toggleClass(INVISIBLE_STATE_CLASS); + this._$closeIcon.toggleClass(INVISIBLE_STATE_CLASS); + } - this.$element().toggleClass(FAB_MAIN_CLASS_WITHOUT_ICON, !this.option('icon')); - } - - _renderCloseIcon() { - this._$closeIcon = this._renderButtonIcon( - this._$closeIcon, - this._options.silent('closeIcon'), - FAB_CLOSE_ICON_CLASS); + _updateZIndexStackPosition() { + super._updateZIndexStackPosition(); + // @ts-expect-error + const overlayStack = this._overlayStack(); - this._$closeIcon.addClass(INVISIBLE_STATE_CLASS); - } + overlayStack.push(this); + } - _renderClick() { - this._clickAction = this._getVisibleActions().length === 1 ? - this._getActionComponent()._createActionByOption('onClick') : - this._createAction(this._clickHandler.bind(this)); + _renderActions() { + const actions = this.option('actions'); + const minActionButtonCount = 1; - this._setClickAction(); + if (this._actionItems && this._actionItems.length) { + this._actionItems.forEach((actionItem) => { + actionItem.dispose(); + actionItem.$element().remove(); + }); + this._actionItems = []; } - _getVisibleActions(actions) { - const currentActions = actions || this.option('actions'); - - return currentActions.filter((action) => action.option('visible')); - } + this._actionItems = []; - _getCurrentOptions(actions) { - const visibleActions = speedDialMainItem._getVisibleActions(actions); + if (actions.length === minActionButtonCount) return; - const defaultOptions = this._getDefaultOptions(); + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + const $actionElement = $('
') + .appendTo(getSwatchContainer(action.$element())); - delete defaultOptions.closeOnOutsideClick; + eventsEngine.off($actionElement, 'click'); + eventsEngine.on($actionElement, 'click', () => { + this._clickHandler(); + }); - return visibleActions.length === 1 ? - extend(modifyActionOptions(visibleActions[0]), { position: this._getPosition() }) : - extend(defaultOptions, { visible: visibleActions.length !== 0 }); + action._options.silent('actionComponent', action); + action._options.silent('parentPosition', this._getPosition()); + action._options.silent('actionVisible', action._options.silent('visible')); + // @ts-expect-error + this._actionItems.push(this._createComponent($actionElement, SpeedDialItem, extend({}, modifyActionOptions(action), { visible: false }))); } + } - _clickHandler() { - const actions = this._actionItems - .filter((action) => action.option('actionVisible')) - .sort((action, nextAction) => action.option('index') - nextAction.option('index')); + _getActionAnimation(action, index, lastActionIndex) { + const actionAnimationDelay = 30; - if(actions.length === 1) return; + action._options.silent('animation.show.delay', actionAnimationDelay * index); + action._options.silent('animation.hide.delay', actionAnimationDelay * (lastActionIndex - index)); - const lastActionIndex = actions.length - 1; - - for(let i = 0; i < actions.length; i++) { - actions[i].option('animation', this._getActionAnimation(actions[i], i, lastActionIndex)); - actions[i].option('position', this._getActionPosition(actions, i)); - actions[i]._$wrapper.css('position', this._$wrapper.css('position')); - actions[i].toggle(); - } - - if(config().floatingActionButtonConfig.shading) { - this._isShadingShown = !this.option('shading'); - this.option('shading', this._isShadingShown); - } + return action._options.silent('animation'); + } - this._$icon.toggleClass(INVISIBLE_STATE_CLASS); - this._$closeIcon.toggleClass(INVISIBLE_STATE_CLASS); - } + _getDirectionIndex(actions, direction) { + const directionIndex = 1; - _updateZIndexStackPosition() { - super._updateZIndexStackPosition(); + if (direction === 'auto') { + // @ts-expect-error + const contentHeight = getHeight(this.$content()); + // @ts-expect-error + const actionsHeight = this.initialOption('indent') + this.initialOption('childIndent') * actions.length - contentHeight; + // @ts-expect-error + const offsetTop = this.$content().offset().top; - const overlayStack = this._overlayStack(); + if (actionsHeight < offsetTop) { + return -directionIndex; + } + // @ts-expect-error + const offsetBottom = getHeight(this._positionController._$wrapperCoveredElement) - contentHeight - offsetTop; - overlayStack.push(this); + return offsetTop >= offsetBottom ? -directionIndex : directionIndex; } - _renderActions() { - const actions = this.option('actions'); - const minActionButtonCount = 1; - - if(this._actionItems && this._actionItems.length) { - this._actionItems.forEach(actionItem => { - actionItem.dispose(); - actionItem.$element().remove(); - }); - this._actionItems = []; - } - - this._actionItems = []; - - if(actions.length === minActionButtonCount) return; - - for(let i = 0; i < actions.length; i++) { - const action = actions[i]; - const $actionElement = $('
') - .appendTo(getSwatchContainer(action.$element())); - - eventsEngine.off($actionElement, 'click'); - eventsEngine.on($actionElement, 'click', () => { - this._clickHandler(); - }); - - action._options.silent('actionComponent', action); - action._options.silent('parentPosition', this._getPosition()); - action._options.silent('actionVisible', action._options.silent('visible')); - - this._actionItems.push(this._createComponent($actionElement, SpeedDialItem, extend({}, modifyActionOptions(action), { visible: false }))); - } + return direction !== 'down' ? -directionIndex : directionIndex; + } + + _getActionPosition(actions, index) { + const action = actions[index]; + // @ts-expect-error + const actionOffsetXValue = this.initialOption('childOffset'); + const actionOffsetX = action._options.silent('label') && !this._$label + ? this._isPositionLeft(this._getPosition()) ? actionOffsetXValue : -actionOffsetXValue + : 0; + // @ts-expect-error + const actionOffsetYValue = this.initialOption('indent') + this.initialOption('childIndent') * index; + const actionOffsetY = this._getDirectionIndex(actions, this.option('direction')) * actionOffsetYValue; + + const actionPositionAtMy = action._options.silent('label') + ? this._isPositionLeft(this._getPosition()) ? 'left' : 'right' + : 'center'; + + return { + // @ts-expect-error + of: this.$content(), + at: actionPositionAtMy, + my: actionPositionAtMy, + offset: { + x: actionOffsetX, + y: actionOffsetY, + }, + }; + } + + _outsideClickHandler(e) { + if (this._isShadingShown) { + const isShadingClick = $(e.target)[0] === this._$wrapper[0]; + + if (isShadingClick) { + e.preventDefault(); + this._clickHandler(); + } } - - _getActionAnimation(action, index, lastActionIndex) { - const actionAnimationDelay = 30; - - action._options.silent('animation.show.delay', actionAnimationDelay * index); - action._options.silent('animation.hide.delay', actionAnimationDelay * (lastActionIndex - index)); - - return action._options.silent('animation'); + } + + _setPosition() { + if (this.option('visible')) { + // @ts-expect-error + this._hide(); + // @ts-expect-error + this._show(); } - - _getDirectionIndex(actions, direction) { - const directionIndex = 1; - - if(direction === 'auto') { - const contentHeight = getHeight(this.$content()); - const actionsHeight = this.initialOption('indent') + this.initialOption('childIndent') * actions.length - contentHeight; - const offsetTop = this.$content().offset().top; - - if(actionsHeight < offsetTop) { - return -directionIndex; - } else { - const offsetBottom = getHeight(this._positionController._$wrapperCoveredElement) - contentHeight - offsetTop; - - return offsetTop >= offsetBottom ? -directionIndex : directionIndex; - } - } - - return direction !== 'down' ? -directionIndex : directionIndex; - } - - _getActionPosition(actions, index) { - const action = actions[index]; - const actionOffsetXValue = this.initialOption('childOffset'); - const actionOffsetX = action._options.silent('label') && !this._$label ? - (this._isPositionLeft(this._getPosition()) ? actionOffsetXValue : -actionOffsetXValue) : - 0; - - const actionOffsetYValue = this.initialOption('indent') + this.initialOption('childIndent') * index; - const actionOffsetY = this._getDirectionIndex(actions, this.option('direction')) * actionOffsetYValue; - - const actionPositionAtMy = action._options.silent('label') ? - (this._isPositionLeft(this._getPosition()) ? 'left' : 'right') : - 'center'; - - return { - of: this.$content(), - at: actionPositionAtMy, - my: actionPositionAtMy, - offset: { - x: actionOffsetX, - y: actionOffsetY - } - }; - } - - _outsideClickHandler(e) { - if(this._isShadingShown) { - const isShadingClick = $(e.target)[0] === this._$wrapper[0]; - - if(isShadingClick) { - e.preventDefault(); - this._clickHandler(); - } - } - } - - _setPosition() { - if(this.option('visible')) { - this._hide(); - this._show(); - } - } - - _getPosition() { - return this._getDefaultOptions().position; - } - - _getInkRippleContainer() { - return this.$content(); - } - - _optionChanged(args) { - switch(args.name) { - case 'actions': - if(this._isVisible()) { - this._renderIcon(); - this._renderLabel(); - } - this._renderCloseIcon(); - this._renderClick(); - this._renderActions(); - break; - case 'maxSpeedDialActionCount': - this._renderActions(); - break; - case 'closeIcon': - this._renderCloseIcon(); - break; - case 'position': - super._optionChanged(args); - this._setPosition(); - break; - case 'label': - if(this._isVisible()) this._renderLabel(); - this._setPosition(); - break; - case 'icon': - if(this._isVisible()) this._renderIcon(); - break; - default: - super._optionChanged(args); + } + + _getPosition() { + return this._getDefaultOptions().position; + } + + _getInkRippleContainer() { + // @ts-expect-error + return this.$content(); + } + + _optionChanged(args) { + switch (args.name) { + case 'actions': + // @ts-expect-error + if (this._isVisible()) { + this._renderIcon(); + this._renderLabel(); } + this._renderCloseIcon(); + this._renderClick(); + this._renderActions(); + break; + case 'maxSpeedDialActionCount': + this._renderActions(); + break; + case 'closeIcon': + this._renderCloseIcon(); + break; + case 'position': + super._optionChanged(args); + this._setPosition(); + break; + case 'label': + // @ts-expect-error + if (this._isVisible()) this._renderLabel(); + this._setPosition(); + break; + case 'icon': + // @ts-expect-error + if (this._isVisible()) this._renderIcon(); + break; + default: + super._optionChanged(args); } + } } export function initAction(newAction) { - // TODO: workaround for Angular/React/Vue - newAction._options.silent('onInitializing', null); - - let isActionExist = false; - if(!speedDialMainItem) { - const $fabMainElement = $('
') - .appendTo(getSwatchContainer(newAction.$element())); - - speedDialMainItem = newAction._createComponent($fabMainElement, SpeedDialMainItem, - extend({}, modifyActionOptions(newAction), { - actions: [ newAction ] - }) - ); - } else { - const savedActions = speedDialMainItem.option('actions'); - - savedActions.forEach(action => { - if(action._options.silent('id') === newAction._options.silent('id')) { - isActionExist = true; - return newAction; - } - }); - - delete speedDialMainItem._options.position; - - if(!isActionExist) { - if(speedDialMainItem._getVisibleActions(savedActions).length >= speedDialMainItem.option('maxSpeedDialActionCount')) { - newAction.dispose(); - errors.log('W1014'); - return; - } - - savedActions.push(newAction); - - speedDialMainItem.option(extend(speedDialMainItem._getCurrentOptions(savedActions), { - actions: savedActions - })); - } else if(savedActions.length === 1) { - speedDialMainItem.option(extend({}, modifyActionOptions(savedActions[0]), { - actions: savedActions, - position: speedDialMainItem._getPosition() - })); - } else { - speedDialMainItem.option(extend(speedDialMainItem._getCurrentOptions(savedActions), { - actions: savedActions - })); - } - } -} - -export function disposeAction(actionId) { - if(!speedDialMainItem) return; + // TODO: workaround for Angular/React/Vue + newAction._options.silent('onInitializing', null); + + let isActionExist = false; + if (!speedDialMainItem) { + const $fabMainElement = $('
') + .appendTo(getSwatchContainer(newAction.$element())); + + speedDialMainItem = newAction._createComponent( + $fabMainElement, + SpeedDialMainItem, + extend({}, modifyActionOptions(newAction), { + actions: [newAction], + }), + ); + } else { + const savedActions = speedDialMainItem.option('actions'); + + savedActions.forEach((action) => { + if (action._options.silent('id') === newAction._options.silent('id')) { + isActionExist = true; + return newAction; + } + }); - let savedActions = speedDialMainItem.option('actions'); - const savedActionsCount = savedActions.length; + delete speedDialMainItem._options.position; + if (!isActionExist) { + // @ts-expect-error + if (speedDialMainItem._getVisibleActions(savedActions).length >= speedDialMainItem.option('maxSpeedDialActionCount')) { + newAction.dispose(); + errors.log('W1014'); + return; + } - savedActions = savedActions.filter(action => { - return action._options.silent('id') !== actionId; - }); + savedActions.push(newAction); - if(savedActionsCount === savedActions.length) return; - - if(!savedActions.length) { - speedDialMainItem.dispose(); - speedDialMainItem.$element().remove(); - speedDialMainItem = null; - } else if(savedActions.length === 1) { - speedDialMainItem.option(extend({}, modifyActionOptions(savedActions[0]), { - actions: savedActions - })); + speedDialMainItem.option(extend(speedDialMainItem._getCurrentOptions(savedActions), { + actions: savedActions, + })); + } else if (savedActions.length === 1) { + speedDialMainItem.option(extend({}, modifyActionOptions(savedActions[0]), { + actions: savedActions, + position: speedDialMainItem._getPosition(), + })); } else { - speedDialMainItem.option({ - actions: savedActions - }); + speedDialMainItem.option(extend(speedDialMainItem._getCurrentOptions(savedActions), { + actions: savedActions, + })); } + } } -export function repaint() { - if(!speedDialMainItem) return; +export function disposeAction(actionId) { + if (!speedDialMainItem) return; - const visibleActions = speedDialMainItem._getVisibleActions(); + let savedActions = speedDialMainItem.option('actions'); + const savedActionsCount = savedActions.length; - const icon = visibleActions.length === 1 ? - visibleActions[0].option('icon') : - speedDialMainItem._getDefaultOptions().icon; + savedActions = savedActions.filter((action) => action._options.silent('id') !== actionId); - const label = visibleActions.length === 1 ? - visibleActions[0].option('label') : - speedDialMainItem._getDefaultOptions().label; + if (savedActionsCount === savedActions.length) return; + if (!savedActions.length) { + speedDialMainItem.dispose(); + speedDialMainItem.$element().remove(); + speedDialMainItem = null; + } else if (savedActions.length === 1) { + speedDialMainItem.option(extend({}, modifyActionOptions(savedActions[0]), { + actions: savedActions, + })); + } else { speedDialMainItem.option({ - actions: speedDialMainItem.option('actions'), - icon, - closeIcon: speedDialMainItem._getDefaultOptions().closeIcon, - position: speedDialMainItem._getPosition(), - label, - maxSpeedDialActionCount: speedDialMainItem._getDefaultOptions().maxSpeedDialActionCount, - direction: speedDialMainItem._getDefaultOptions().direction + actions: savedActions, }); + } +} + +export function repaint() { + if (!speedDialMainItem) return; + + const visibleActions = speedDialMainItem._getVisibleActions(); + + const icon = visibleActions.length === 1 + ? visibleActions[0].option('icon') + : speedDialMainItem._getDefaultOptions().icon; + + const label = visibleActions.length === 1 + ? visibleActions[0].option('label') + : speedDialMainItem._getDefaultOptions().label; + // @ts-expect-error + speedDialMainItem.option({ + actions: speedDialMainItem.option('actions'), + icon, + closeIcon: speedDialMainItem._getDefaultOptions().closeIcon, + position: speedDialMainItem._getPosition(), + label, + maxSpeedDialActionCount: speedDialMainItem._getDefaultOptions().maxSpeedDialActionCount, + direction: speedDialMainItem._getDefaultOptions().direction, + }); } diff --git a/packages/devextreme/js/__internal/ui/toast/m_toast.ts b/packages/devextreme/js/__internal/ui/toast/m_toast.ts index 9d7e8bb32918..a6df10c0a7bd 100644 --- a/packages/devextreme/js/__internal/ui/toast/m_toast.ts +++ b/packages/devextreme/js/__internal/ui/toast/m_toast.ts @@ -56,7 +56,7 @@ ready(() => { } }); }); - +// @ts-expect-error const Toast = Overlay.inherit({ _getDefaultOptions() { diff --git a/packages/devextreme/js/ui/speed_dial_action.js b/packages/devextreme/js/ui/speed_dial_action.js index ce6a657146e3..457dd5c21a1d 100644 --- a/packages/devextreme/js/ui/speed_dial_action.js +++ b/packages/devextreme/js/ui/speed_dial_action.js @@ -1,2 +1,20 @@ -import SpeedDialAction from './speed_dial_action/speed_dial_action'; +import SpeedDialAction from '../__internal/ui/speed_dial_action/m_speed_dial_action'; + export default SpeedDialAction; + +// STYLE speedDialAction + +/** + * @name dxSpeedDialActionOptions.width + * @hidden + */ + +/** + * @name dxSpeedDialActionOptions.height + * @hidden + */ + +/** + * @name dxSpeedDialActionOptions.disabled + * @hidden + */ diff --git a/packages/devextreme/js/ui/speed_dial_action/repaint_floating_action_button.js b/packages/devextreme/js/ui/speed_dial_action/repaint_floating_action_button.js new file mode 100644 index 000000000000..c57c8e890ad5 --- /dev/null +++ b/packages/devextreme/js/ui/speed_dial_action/repaint_floating_action_button.js @@ -0,0 +1,3 @@ +import repaintFloatingActionButton from '../../__internal/ui/speed_dial_action/m_repaint_floating_action_button'; + +export default repaintFloatingActionButton; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/speedDialAction.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/speedDialAction.tests.js index 0cc7b03c3d37..2af32bbbf09e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/speedDialAction.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/speedDialAction.tests.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import config from 'core/config'; import fx from 'animation/fx'; -import SpeedDialItem from 'ui/speed_dial_action/speed_dial_item'; +import SpeedDialItem from '__internal/ui/speed_dial_action/m_speed_dial_item'; import { logger } from 'core/utils/console'; import 'ui/speed_dial_action'; From f22d4bce2bf2778b46e892057504cdc660d928fa Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 13:01:38 +0400 Subject: [PATCH 20/32] Editor, DataExpressionMixin: move files to TS --- .../ui/editor/m_data_expression.ts} | 0 .../js/{ui/editor/editor.js => __internal/ui/editor/m_editor.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/editor/ui.data_expression.js => __internal/ui/editor/m_data_expression.ts} (100%) rename packages/devextreme/js/{ui/editor/editor.js => __internal/ui/editor/m_editor.ts} (100%) diff --git a/packages/devextreme/js/ui/editor/ui.data_expression.js b/packages/devextreme/js/__internal/ui/editor/m_data_expression.ts similarity index 100% rename from packages/devextreme/js/ui/editor/ui.data_expression.js rename to packages/devextreme/js/__internal/ui/editor/m_data_expression.ts diff --git a/packages/devextreme/js/ui/editor/editor.js b/packages/devextreme/js/__internal/ui/editor/m_editor.ts similarity index 100% rename from packages/devextreme/js/ui/editor/editor.js rename to packages/devextreme/js/__internal/ui/editor/m_editor.ts From 93acf41bdfe3e7f8768cd48b3582a173d2fc8a2d Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 13:07:16 +0400 Subject: [PATCH 21/32] Editor, DataExpressionMixin: ignore errors after move to TS --- .../__internal/ui/editor/m_data_expression.ts | 381 ++++----- .../js/__internal/ui/editor/m_editor.ts | 728 +++++++++--------- packages/devextreme/js/ui/editor/editor.js | 10 + .../js/ui/editor/ui.data_expression.js | 3 + 4 files changed, 567 insertions(+), 555 deletions(-) create mode 100644 packages/devextreme/js/ui/editor/editor.js create mode 100644 packages/devextreme/js/ui/editor/ui.data_expression.js diff --git a/packages/devextreme/js/__internal/ui/editor/m_data_expression.ts b/packages/devextreme/js/__internal/ui/editor/m_data_expression.ts index 52ee0b6f04c6..dac4f256247e 100644 --- a/packages/devextreme/js/__internal/ui/editor/m_data_expression.ts +++ b/packages/devextreme/js/__internal/ui/editor/m_data_expression.ts @@ -1,204 +1,211 @@ -import variableWrapper from '../../core/utils/variable_wrapper'; -import { compileGetter, toComparable } from '../../core/utils/data'; -import { ensureDefined, noop } from '../../core/utils/common'; -import { isDefined, isObject as isObjectType, isString, isFunction } from '../../core/utils/type'; -import { extend } from '../../core/utils/extend'; -import DataHelperMixin from '../../data_helper'; -import { DataSource } from '../../data/data_source/data_source'; -import ArrayStore from '../../data/array_store'; -import { Deferred } from '../../core/utils/deferred'; +import { ensureDefined, noop } from '@js/core/utils/common'; +import { + compileGetter, + // @ts-expect-error + toComparable, +} from '@js/core/utils/data'; +import { Deferred } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { + isDefined, isFunction, isObject as isObjectType, isString, +} from '@js/core/utils/type'; +import variableWrapper from '@js/core/utils/variable_wrapper'; +import ArrayStore from '@js/data/array_store'; +import { DataSource } from '@js/data/data_source/data_source'; +import DataHelperMixin from '@js/data_helper'; const DataExpressionMixin = extend({}, DataHelperMixin, { - _dataExpressionDefaultOptions: function() { - return { - items: [], + _dataExpressionDefaultOptions() { + return { + items: [], - dataSource: null, + dataSource: null, - itemTemplate: 'item', + itemTemplate: 'item', - value: null, + value: null, - valueExpr: 'this', + valueExpr: 'this', - displayExpr: undefined - }; - }, + displayExpr: undefined, + }; + }, - _initDataExpressions: function() { - this._compileValueGetter(); - this._compileDisplayGetter(); - this._initDynamicTemplates(); - this._initDataSource(); - this._itemsToDataSource(); - }, - - _itemsToDataSource: function() { - if(!this.option('dataSource')) { - // TODO: try this.option("dataSource", new ...) - this._dataSource = new DataSource({ - store: new ArrayStore(this.option('items')), - pageSize: 0 - }); - this._initDataController(); - } - }, - - _compileDisplayGetter: function() { - this._displayGetter = compileGetter(this._displayGetterExpr()); - }, - - _displayGetterExpr: function() { - return this.option('displayExpr'); - }, - - _compileValueGetter: function() { - this._valueGetter = compileGetter(this._valueGetterExpr()); - }, - - _valueGetterExpr: function() { - return this.option('valueExpr') || 'this'; - }, - - _loadValue: function(value) { - const deferred = new Deferred(); - value = this._unwrappedValue(value); - - if(!isDefined(value)) { - return deferred.reject().promise(); - } - - this._loadSingle(this._valueGetterExpr(), value) - .done((function(item) { - this._isValueEquals(this._valueGetter(item), value) - ? deferred.resolve(item) - : deferred.reject(); - }).bind(this)) - .fail(function() { - deferred.reject(); - }); - - this._loadValueDeferred = deferred; - return deferred.promise(); - }, - - _rejectValueLoading: function() { - this._loadValueDeferred?.reject({ shouldSkipCallback: true }); - }, - - _getCurrentValue: function() { - return this.option('value'); - }, - - _unwrappedValue: function(value) { - value = value ?? this._getCurrentValue(); - - if(value && this._dataSource && this._valueGetterExpr() === 'this') { - value = this._getItemKey(value); - } - - return variableWrapper.unwrap(value); - }, - - _getItemKey: function(value) { - const key = this._dataSource.key(); - - if(Array.isArray(key)) { - const result = {}; - for(let i = 0, n = key.length; i < n; i++) { - result[key[i]] = value[key[i]]; - } - return result; - } - - if(key && typeof value === 'object') { - value = value[key]; - } - - return value; - }, - - _isValueEquals: function(value1, value2) { - const dataSourceKey = this._dataSource && this._dataSource.key(); - - let result = this._compareValues(value1, value2); - - if(!result && dataSourceKey && isDefined(value1) && isDefined(value2)) { - if(Array.isArray(dataSourceKey)) { - result = this._compareByCompositeKey(value1, value2, dataSourceKey); - } else { - result = this._compareByKey(value1, value2, dataSourceKey); - } - } - - return result; - }, - - _compareByCompositeKey: function(value1, value2, key) { - const isObject = isObjectType; - - if(!isObject(value1) || !isObject(value2)) { - return false; - } - - for(let i = 0, n = key.length; i < n; i++) { - if(value1[key[i]] !== value2[key[i]]) { - return false; - } - } + _initDataExpressions() { + this._compileValueGetter(); + this._compileDisplayGetter(); + this._initDynamicTemplates(); + this._initDataSource(); + this._itemsToDataSource(); + }, + + _itemsToDataSource() { + if (!this.option('dataSource')) { + // TODO: try this.option("dataSource", new ...) + this._dataSource = new DataSource({ + store: new ArrayStore(this.option('items')), + pageSize: 0, + }); + this._initDataController(); + } + }, + + _compileDisplayGetter() { + this._displayGetter = compileGetter(this._displayGetterExpr()); + }, + + _displayGetterExpr() { + return this.option('displayExpr'); + }, + + _compileValueGetter() { + this._valueGetter = compileGetter(this._valueGetterExpr()); + }, + + _valueGetterExpr() { + return this.option('valueExpr') || 'this'; + }, + + _loadValue(value) { + const deferred = Deferred(); + value = this._unwrappedValue(value); + + if (!isDefined(value)) { + return deferred.reject().promise(); + } + + this._loadSingle(this._valueGetterExpr(), value) + .done((item) => { + this._isValueEquals(this._valueGetter(item), value) + ? deferred.resolve(item) + : deferred.reject(); + }) + .fail(() => { + deferred.reject(); + }); + + this._loadValueDeferred = deferred; + return deferred.promise(); + }, + + _rejectValueLoading() { + this._loadValueDeferred?.reject({ shouldSkipCallback: true }); + }, + + _getCurrentValue() { + return this.option('value'); + }, + + _unwrappedValue(value) { + value = value ?? this._getCurrentValue(); + + if (value && this._dataSource && this._valueGetterExpr() === 'this') { + value = this._getItemKey(value); + } + + return variableWrapper.unwrap(value); + }, + + _getItemKey(value) { + const key = this._dataSource.key(); + + if (Array.isArray(key)) { + const result = {}; + for (let i = 0, n = key.length; i < n; i++) { + result[key[i]] = value[key[i]]; + } + return result; + } + + if (key && typeof value === 'object') { + value = value[key]; + } - return true; - }, - - _compareByKey: function(value1, value2, key) { - const unwrapObservable = variableWrapper.unwrap; - const valueKey1 = ensureDefined(unwrapObservable(value1[key]), value1); - const valueKey2 = ensureDefined(unwrapObservable(value2[key]), value2); + return value; + }, - return this._compareValues(valueKey1, valueKey2); - }, - - _compareValues: function(value1, value2) { - return toComparable(value1, true) === toComparable(value2, true); - }, - - _initDynamicTemplates: noop, + _isValueEquals(value1, value2) { + const dataSourceKey = this._dataSource && this._dataSource.key(); - _setCollectionWidgetItemTemplate: function() { + let result = this._compareValues(value1, value2); + + if (!result && dataSourceKey && isDefined(value1) && isDefined(value2)) { + if (Array.isArray(dataSourceKey)) { + result = this._compareByCompositeKey(value1, value2, dataSourceKey); + } else { + result = this._compareByKey(value1, value2, dataSourceKey); + } + } + + return result; + }, + + _compareByCompositeKey(value1, value2, key) { + const isObject = isObjectType; + + if (!isObject(value1) || !isObject(value2)) { + return false; + } + + for (let i = 0, n = key.length; i < n; i++) { + if (value1[key[i]] !== value2[key[i]]) { + return false; + } + } + + return true; + }, + + _compareByKey(value1, value2, key) { + const unwrapObservable = variableWrapper.unwrap; + const valueKey1 = ensureDefined(unwrapObservable(value1[key]), value1); + const valueKey2 = ensureDefined(unwrapObservable(value2[key]), value2); + + return this._compareValues(valueKey1, valueKey2); + }, + + _compareValues(value1, value2) { + return toComparable(value1, true) === toComparable(value2, true); + }, + + _initDynamicTemplates: noop, + + _setCollectionWidgetItemTemplate() { + this._initDynamicTemplates(); + this._setCollectionWidgetOption('itemTemplate', this.option('itemTemplate')); + }, + + _getCollectionKeyExpr() { + const valueExpr = this.option('valueExpr'); + const isValueExprField = isString(valueExpr) && valueExpr !== 'this' || isFunction(valueExpr); + + return isValueExprField ? valueExpr : null; + }, + + _dataExpressionOptionChanged(args) { + // eslint-disable-next-line default-case + switch (args.name) { + case 'items': + this._itemsToDataSource(); + this._setCollectionWidgetOption('items'); + break; + case 'dataSource': + this._initDataSource(); + break; + case 'itemTemplate': + this._setCollectionWidgetItemTemplate(); + break; + case 'valueExpr': + this._compileValueGetter(); + break; + case 'displayExpr': + this._compileDisplayGetter(); this._initDynamicTemplates(); - this._setCollectionWidgetOption('itemTemplate', this.option('itemTemplate')); - }, - - _getCollectionKeyExpr: function() { - const valueExpr = this.option('valueExpr'); - const isValueExprField = isString(valueExpr) && valueExpr !== 'this' || isFunction(valueExpr); - - return isValueExprField ? valueExpr : null; - }, - - _dataExpressionOptionChanged: function(args) { - switch(args.name) { - case 'items': - this._itemsToDataSource(); - this._setCollectionWidgetOption('items'); - break; - case 'dataSource': - this._initDataSource(); - break; - case 'itemTemplate': - this._setCollectionWidgetItemTemplate(); - break; - case 'valueExpr': - this._compileValueGetter(); - break; - case 'displayExpr': - this._compileDisplayGetter(); - this._initDynamicTemplates(); - this._setCollectionWidgetOption('displayExpr'); - break; - } + this._setCollectionWidgetOption('displayExpr'); + break; } + }, }); export default DataExpressionMixin; diff --git a/packages/devextreme/js/__internal/ui/editor/m_editor.ts b/packages/devextreme/js/__internal/ui/editor/m_editor.ts index 8c5a75f957d8..98d0c322468b 100644 --- a/packages/devextreme/js/__internal/ui/editor/m_editor.ts +++ b/packages/devextreme/js/__internal/ui/editor/m_editor.ts @@ -1,16 +1,16 @@ -import $ from '../../core/renderer'; -import { data } from '../../core/element_data'; -import Callbacks from '../../core/utils/callbacks'; -import { hasWindow } from '../../core/utils/window'; -import { addNamespace, normalizeKeyName } from '../../events/utils/index'; -import { extend } from '../../core/utils/extend'; -import Widget from '../widget/ui.widget'; -import ValidationEngine from '../validation_engine'; -import EventsEngine from '../../events/core/events_engine'; -import ValidationMessage from '../validation_message'; -import Guid from '../../core/guid'; -import { noop } from '../../core/utils/common'; -import { resetActiveElement } from '../../core/utils/dom'; +import { data } from '@js/core/element_data'; +import Guid from '@js/core/guid'; +import $ from '@js/core/renderer'; +import Callbacks from '@js/core/utils/callbacks'; +import { noop } from '@js/core/utils/common'; +import { resetActiveElement } from '@js/core/utils/dom'; +import { extend } from '@js/core/utils/extend'; +import { hasWindow } from '@js/core/utils/window'; +import EventsEngine from '@js/events/core/events_engine'; +import { addNamespace, normalizeKeyName } from '@js/events/utils/index'; +import ValidationEngine from '@js/ui/validation_engine'; +import ValidationMessage from '@js/ui/validation_message'; +import Widget from '@js/ui/widget/ui.widget'; const INVALID_MESSAGE_AUTO = 'dx-invalid-message-auto'; const READONLY_STATE_CLASS = 'dx-state-readonly'; @@ -24,398 +24,390 @@ const READONLY_NAMESPACE = 'editorReadOnly'; const ALLOWED_STYLING_MODES = ['outlined', 'filled', 'underlined']; const VALIDATION_MESSAGE_KEYS_MAP = { - validationMessageMode: 'mode', - validationMessagePosition: 'positionSide', - validationMessageOffset: 'offset', - validationBoundary: 'boundary', + validationMessageMode: 'mode', + validationMessagePosition: 'positionSide', + validationMessageOffset: 'offset', + validationBoundary: 'boundary', }; - +// @ts-expect-error const Editor = Widget.inherit({ - ctor: function() { - this.showValidationMessageTimeout = null; - this.validationRequest = Callbacks(); - - this.callBase.apply(this, arguments); - }, - - _createElement: function(element) { - this.callBase(element); - const $element = this.$element(); - if($element) { - data($element[0], VALIDATION_TARGET, this); - } - }, - - _initOptions: function(options) { - this.callBase.apply(this, arguments); - this.option(ValidationEngine.initValidationOptions(options)); - }, - - _init: function() { - this._initialValue = this.option('value'); - this.callBase(); - this._options.cache('validationTooltipOptions', this.option('validationTooltipOptions')); - const $element = this.$element(); - $element.addClass(DX_INVALID_BADGE_CLASS); - }, - - _getDefaultOptions: function() { - return extend(this.callBase(), { - value: null, - - /** - * @name EditorOptions.name - * @type string - * @default "" - * @hidden - */ - name: '', + ctor() { + this.showValidationMessageTimeout = null; + this.validationRequest = Callbacks(); - onValueChanged: null, + this.callBase.apply(this, arguments); + }, - readOnly: false, + _createElement(element) { + this.callBase(element); + const $element = this.$element(); + if ($element) { + data($element[0], VALIDATION_TARGET, this); + } + }, - isValid: true, + _initOptions(options) { + this.callBase.apply(this, arguments); + // @ts-expect-error + this.option(ValidationEngine.initValidationOptions(options)); + }, - validationError: null, + _init() { + this._initialValue = this.option('value'); + this.callBase(); + this._options.cache('validationTooltipOptions', this.option('validationTooltipOptions')); + const $element = this.$element(); + $element.addClass(DX_INVALID_BADGE_CLASS); + }, - validationErrors: null, + _getDefaultOptions() { + return extend(this.callBase(), { + value: null, - validationStatus: VALIDATION_STATUS_VALID, + name: '', - validationMessageMode: 'auto', + onValueChanged: null, - validationMessagePosition: 'bottom', + readOnly: false, - validationBoundary: undefined, + isValid: true, - validationMessageOffset: { h: 0, v: 0 }, + validationError: null, - validationTooltipOptions: {}, + validationErrors: null, - _showValidationMessage: true, + validationStatus: VALIDATION_STATUS_VALID, - isDirty: false - }); - }, + validationMessageMode: 'auto', - _attachKeyboardEvents: function() { - if(!this.option('readOnly')) { - this.callBase(); - } - }, + validationMessagePosition: 'bottom', - _setOptionsByReference: function() { - this.callBase(); + validationBoundary: undefined, - extend(this._optionsByReference, { - validationError: true - }); - }, + validationMessageOffset: { h: 0, v: 0 }, - _createValueChangeAction: function() { - this._valueChangeAction = this._createActionByOption('onValueChanged', { - excludeValidators: ['disabled', 'readOnly'] - }); - }, + validationTooltipOptions: {}, - _suppressValueChangeAction: function() { - this._valueChangeActionSuppressed = true; - }, + _showValidationMessage: true, - _resumeValueChangeAction: function() { - this._valueChangeActionSuppressed = false; - }, + isDirty: false, + }); + }, - _initMarkup: function() { - this._toggleReadOnlyState(); - this._setSubmitElementName(this.option('name')); + _attachKeyboardEvents() { + if (!this.option('readOnly')) { + this.callBase(); + } + }, - this.callBase(); - this._renderValidationState(); - this.option('_onMarkupRendered')?.(); - }, + _setOptionsByReference() { + this.callBase(); - _raiseValueChangeAction: function(value, previousValue) { - if(!this._valueChangeAction) { - this._createValueChangeAction(); - } - this._valueChangeAction(this._valueChangeArgs(value, previousValue)); - }, - - _valueChangeArgs: function(value, previousValue) { - return { - value: value, - previousValue: previousValue, - event: this._valueChangeEventInstance - }; - }, - - _saveValueChangeEvent: function(e) { - this._valueChangeEventInstance = e; - }, - - _focusInHandler: function(e) { - const isValidationMessageShownOnFocus = this.option('validationMessageMode') === 'auto'; - - // NOTE: The click should be processed before the validation message is shown because - // it can change the editor's value - if(this._canValueBeChangedByClick() && isValidationMessageShownOnFocus) { - // NOTE: Prevent the validation message from showing - const $validationMessageWrapper = this._validationMessage?.$wrapper(); - $validationMessageWrapper?.removeClass(INVALID_MESSAGE_AUTO); - - clearTimeout(this.showValidationMessageTimeout); - - // NOTE: Show the validation message after a click changes the value - this.showValidationMessageTimeout = setTimeout( - () => $validationMessageWrapper?.addClass(INVALID_MESSAGE_AUTO), 150 - ); - } + extend(this._optionsByReference, { + validationError: true, + }); + }, - return this.callBase(e); - }, + _createValueChangeAction() { + this._valueChangeAction = this._createActionByOption('onValueChanged', { + excludeValidators: ['disabled', 'readOnly'], + }); + }, - _canValueBeChangedByClick: function() { - return false; - }, + _suppressValueChangeAction() { + this._valueChangeActionSuppressed = true; + }, - _getStylingModePrefix: function() { - return 'dx-editor-'; - }, + _resumeValueChangeAction() { + this._valueChangeActionSuppressed = false; + }, - _renderStylingMode: function() { - const optionName = 'stylingMode'; - const optionValue = this.option(optionName); - const prefix = this._getStylingModePrefix(); + _initMarkup() { + this._toggleReadOnlyState(); + this._setSubmitElementName(this.option('name')); - const allowedStylingClasses = ALLOWED_STYLING_MODES.map((mode) => { - return prefix + mode; - }); + this.callBase(); + this._renderValidationState(); + this.option('_onMarkupRendered')?.(); + }, - allowedStylingClasses.forEach(className => this.$element().removeClass(className)); + _raiseValueChangeAction(value, previousValue) { + if (!this._valueChangeAction) { + this._createValueChangeAction(); + } + this._valueChangeAction(this._valueChangeArgs(value, previousValue)); + }, - let stylingModeClass = prefix + optionValue; + _valueChangeArgs(value, previousValue) { + return { + value, + previousValue, + event: this._valueChangeEventInstance, + }; + }, - if(allowedStylingClasses.indexOf(stylingModeClass) === -1) { - const defaultOptionValue = this._getDefaultOptions()[optionName]; - const platformOptionValue = this._convertRulesToOptions(this._defaultOptionsRules())[optionName]; - stylingModeClass = prefix + (platformOptionValue || defaultOptionValue); - } + _saveValueChangeEvent(e) { + this._valueChangeEventInstance = e; + }, + + _focusInHandler(e) { + const isValidationMessageShownOnFocus = this.option('validationMessageMode') === 'auto'; + + // NOTE: The click should be processed before the validation message is shown because + // it can change the editor's value + if (this._canValueBeChangedByClick() && isValidationMessageShownOnFocus) { + // NOTE: Prevent the validation message from showing + const $validationMessageWrapper = this._validationMessage?.$wrapper(); + $validationMessageWrapper?.removeClass(INVALID_MESSAGE_AUTO); - this.$element().addClass(stylingModeClass); - }, + clearTimeout(this.showValidationMessageTimeout); + + // NOTE: Show the validation message after a click changes the value + this.showValidationMessageTimeout = setTimeout(() => $validationMessageWrapper?.addClass(INVALID_MESSAGE_AUTO), 150); + } - _getValidationErrors: function() { - let validationErrors = this.option('validationErrors'); - if(!validationErrors && this.option('validationError')) { - validationErrors = [this.option('validationError')]; + return this.callBase(e); + }, + + _canValueBeChangedByClick() { + return false; + }, + + _getStylingModePrefix() { + return 'dx-editor-'; + }, + + _renderStylingMode() { + const optionName = 'stylingMode'; + const optionValue = this.option(optionName); + const prefix = this._getStylingModePrefix(); + + const allowedStylingClasses = ALLOWED_STYLING_MODES.map((mode) => prefix + mode); + + allowedStylingClasses.forEach((className) => this.$element().removeClass(className)); + + let stylingModeClass = prefix + optionValue; + + if (!allowedStylingClasses.includes(stylingModeClass)) { + const defaultOptionValue = this._getDefaultOptions()[optionName]; + const platformOptionValue = this._convertRulesToOptions(this._defaultOptionsRules())[optionName]; + stylingModeClass = prefix + (platformOptionValue || defaultOptionValue); + } + + this.$element().addClass(stylingModeClass); + }, + + _getValidationErrors() { + let validationErrors = this.option('validationErrors'); + if (!validationErrors && this.option('validationError')) { + validationErrors = [this.option('validationError')]; + } + return validationErrors; + }, + + _disposeValidationMessage() { + if (this._$validationMessage) { + this._$validationMessage.remove(); + this.setAria('describedby', null); + this._$validationMessage = undefined; + this._validationMessage = undefined; + } + }, + + _toggleValidationClasses(isInvalid) { + this.$element().toggleClass(INVALID_CLASS, isInvalid); + this.setAria(VALIDATION_STATUS_INVALID, isInvalid || undefined); + }, + + _renderValidationState() { + const isValid = this.option('isValid') && this.option('validationStatus') !== VALIDATION_STATUS_INVALID; + const validationErrors = this._getValidationErrors(); + const $element = this.$element(); + + this._toggleValidationClasses(!isValid); + + if (!hasWindow() || this.option('_showValidationMessage') === false) { + return; + } + + this._disposeValidationMessage(); + if (!isValid && validationErrors) { + const { + validationMessageMode, validationMessageOffset, validationBoundary, rtlEnabled, + } = this.option(); + + this._$validationMessage = $('
').appendTo($element); + const validationMessageContentId = `dx-${new Guid()}`; + this.setAria('describedby', validationMessageContentId); + + this._validationMessage = new ValidationMessage(this._$validationMessage, extend({ + validationErrors, + rtlEnabled, + target: this._getValidationMessageTarget(), + visualContainer: $element, + mode: validationMessageMode, + positionSide: this._getValidationMessagePosition(), + offset: validationMessageOffset, + boundary: validationBoundary, + contentId: validationMessageContentId, + }, this._options.cache('validationTooltipOptions'))); + this._bindInnerWidgetOptions(this._validationMessage, 'validationTooltipOptions'); + } + }, + + _getValidationMessagePosition() { + return this.option('validationMessagePosition'); + }, + + _getValidationMessageTarget() { + return this.$element(); + }, + + _toggleReadOnlyState() { + const readOnly = this.option('readOnly'); + + this._toggleBackspaceHandler(readOnly); + this.$element().toggleClass(READONLY_STATE_CLASS, !!readOnly); + this._setAriaReadonly(readOnly); + }, + + _setAriaReadonly(readOnly) { + this.setAria('readonly', readOnly || undefined); + }, + + _toggleBackspaceHandler(isReadOnly) { + const $eventTarget = this._keyboardEventBindingTarget(); + const eventName = addNamespace('keydown', READONLY_NAMESPACE); + + EventsEngine.off($eventTarget, eventName); + + if (isReadOnly) { + EventsEngine.on($eventTarget, eventName, (e) => { + if (normalizeKeyName(e) === 'backspace') { + e.preventDefault(); } - return validationErrors; - }, - - _disposeValidationMessage: function() { - if(this._$validationMessage) { - this._$validationMessage.remove(); - this.setAria('describedby', null); - this._$validationMessage = undefined; - this._validationMessage = undefined; - } - }, - - _toggleValidationClasses: function(isInvalid) { - this.$element().toggleClass(INVALID_CLASS, isInvalid); - this.setAria(VALIDATION_STATUS_INVALID, isInvalid || undefined); - }, - - _renderValidationState: function() { - const isValid = this.option('isValid') && this.option('validationStatus') !== VALIDATION_STATUS_INVALID; - const validationErrors = this._getValidationErrors(); - const $element = this.$element(); - - this._toggleValidationClasses(!isValid); - - if(!hasWindow() || this.option('_showValidationMessage') === false) { - return; - } - - this._disposeValidationMessage(); - if(!isValid && validationErrors) { - const { validationMessageMode, validationMessageOffset, validationBoundary, rtlEnabled } = this.option(); - - this._$validationMessage = $('
').appendTo($element); - const validationMessageContentId = `dx-${new Guid()}`; - this.setAria('describedby', validationMessageContentId); - - this._validationMessage = new ValidationMessage(this._$validationMessage, extend({ - validationErrors, - rtlEnabled, - target: this._getValidationMessageTarget(), - visualContainer: $element, - mode: validationMessageMode, - positionSide: this._getValidationMessagePosition(), - offset: validationMessageOffset, - boundary: validationBoundary, - contentId: validationMessageContentId - }, this._options.cache('validationTooltipOptions'))); - this._bindInnerWidgetOptions(this._validationMessage, 'validationTooltipOptions'); - } - }, - - _getValidationMessagePosition: function() { - return this.option('validationMessagePosition'); - }, - - _getValidationMessageTarget: function() { - return this.$element(); - }, - - _toggleReadOnlyState: function() { - const readOnly = this.option('readOnly'); - - this._toggleBackspaceHandler(readOnly); - this.$element().toggleClass(READONLY_STATE_CLASS, !!readOnly); - this._setAriaReadonly(readOnly); - }, - - _setAriaReadonly(readOnly) { - this.setAria('readonly', readOnly || undefined); - }, - - _toggleBackspaceHandler: function(isReadOnly) { - const $eventTarget = this._keyboardEventBindingTarget(); - const eventName = addNamespace('keydown', READONLY_NAMESPACE); - - EventsEngine.off($eventTarget, eventName); - - if(isReadOnly) { - EventsEngine.on($eventTarget, eventName, (e) => { - if(normalizeKeyName(e) === 'backspace') { - e.preventDefault(); - } - }); - } - }, - - _dispose: function() { - const element = this.$element()[0]; - - data(element, VALIDATION_TARGET, null); - clearTimeout(this.showValidationMessageTimeout); - this._disposeValidationMessage(); - this.callBase(); - }, - - _setSubmitElementName: function(name) { - const $submitElement = this._getSubmitElement(); - - if(!$submitElement) { - return; - } - - if(name.length > 0) { - $submitElement.attr('name', name); - } else { - $submitElement.removeAttr('name'); - } - }, - - _getSubmitElement: function() { - return null; - }, - - _setValidationMessageOption: function({ name, value }) { - const optionKey = VALIDATION_MESSAGE_KEYS_MAP[name] ? VALIDATION_MESSAGE_KEYS_MAP[name] : name; - this._validationMessage?.option(optionKey, value); - }, - - _hasActiveElement: noop, - - _optionChanged: function(args) { - switch(args.name) { - case 'onValueChanged': - this._createValueChangeAction(); - break; - case 'readOnly': - this._toggleReadOnlyState(); - this._refreshFocusState(); - break; - case 'value': - if(args.value != args.previousValue) { // eslint-disable-line eqeqeq - this.option('isDirty', this._initialValue !== args.value); - - this.validationRequest.fire({ - value: args.value, - editor: this - }); - } - if(!this._valueChangeActionSuppressed) { - this._raiseValueChangeAction(args.value, args.previousValue); - this._saveValueChangeEvent(undefined); - } - break; - case 'width': - this.callBase(args); - this._validationMessage?.updateMaxWidth(); - break; - case 'name': - this._setSubmitElementName(args.value); - break; - case 'isValid': - case 'validationError': - case 'validationErrors': - case 'validationStatus': - this.option(ValidationEngine.synchronizeValidationOptions(args, this.option())); - this._renderValidationState(); - break; - case 'validationBoundary': - case 'validationMessageMode': - case 'validationMessagePosition': - case 'validationMessageOffset': - this._setValidationMessageOption(args); - break; - case 'rtlEnabled': - this._setValidationMessageOption(args); - this.callBase(args); - break; - case 'validationTooltipOptions': - this._innerWidgetOptionChanged(this._validationMessage, args); - break; - case '_showValidationMessage': - case 'isDirty': - break; - default: - this.callBase(args); + }); + } + }, + + _dispose() { + const element = this.$element()[0]; + + data(element, VALIDATION_TARGET, null); + clearTimeout(this.showValidationMessageTimeout); + this._disposeValidationMessage(); + this.callBase(); + }, + + _setSubmitElementName(name) { + const $submitElement = this._getSubmitElement(); + + if (!$submitElement) { + return; + } + + if (name.length > 0) { + $submitElement.attr('name', name); + } else { + $submitElement.removeAttr('name'); + } + }, + + _getSubmitElement() { + return null; + }, + + _setValidationMessageOption({ name, value }) { + const optionKey = VALIDATION_MESSAGE_KEYS_MAP[name] ? VALIDATION_MESSAGE_KEYS_MAP[name] : name; + this._validationMessage?.option(optionKey, value); + }, + + _hasActiveElement: noop, + + _optionChanged(args) { + switch (args.name) { + case 'onValueChanged': + this._createValueChangeAction(); + break; + case 'readOnly': + this._toggleReadOnlyState(); + this._refreshFocusState(); + break; + case 'value': + if (args.value != args.previousValue) { // eslint-disable-line eqeqeq + this.option('isDirty', this._initialValue !== args.value); + + this.validationRequest.fire({ + value: args.value, + editor: this, + }); } - }, - - _resetToInitialValue: function() { - this.option('value', this._initialValue); - }, - - blur: function() { - if(this._hasActiveElement()) { - resetActiveElement(); + if (!this._valueChangeActionSuppressed) { + this._raiseValueChangeAction(args.value, args.previousValue); + this._saveValueChangeEvent(undefined); } - }, - - clear: function() { - const defaultOptions = this._getDefaultOptions(); - this.option('value', defaultOptions.value); - }, - - reset: function(value = undefined) { - if(arguments.length) { - this._initialValue = value; - } - - this._resetToInitialValue(); - this.option('isDirty', false); - this.option('isValid', true); - }, + break; + case 'width': + this.callBase(args); + this._validationMessage?.updateMaxWidth(); + break; + case 'name': + this._setSubmitElementName(args.value); + break; + case 'isValid': + case 'validationError': + case 'validationErrors': + case 'validationStatus': + // @ts-expect-error + this.option(ValidationEngine.synchronizeValidationOptions(args, this.option())); + this._renderValidationState(); + break; + case 'validationBoundary': + case 'validationMessageMode': + case 'validationMessagePosition': + case 'validationMessageOffset': + this._setValidationMessageOption(args); + break; + case 'rtlEnabled': + this._setValidationMessageOption(args); + this.callBase(args); + break; + case 'validationTooltipOptions': + this._innerWidgetOptionChanged(this._validationMessage, args); + break; + case '_showValidationMessage': + case 'isDirty': + break; + default: + this.callBase(args); + } + }, + + _resetToInitialValue() { + this.option('value', this._initialValue); + }, + + blur() { + if (this._hasActiveElement()) { + resetActiveElement(); + } + }, + + clear() { + const defaultOptions = this._getDefaultOptions(); + this.option('value', defaultOptions.value); + }, + + reset(value = undefined) { + if (arguments.length) { + this._initialValue = value; + } + + this._resetToInitialValue(); + this.option('isDirty', false); + this.option('isValid', true); + }, }); -Editor.isEditor = (instance) => { - return instance instanceof Editor; -}; +Editor.isEditor = (instance) => instance instanceof Editor; export default Editor; diff --git a/packages/devextreme/js/ui/editor/editor.js b/packages/devextreme/js/ui/editor/editor.js new file mode 100644 index 000000000000..a82dafb68d32 --- /dev/null +++ b/packages/devextreme/js/ui/editor/editor.js @@ -0,0 +1,10 @@ +import Editor from '../../__internal/ui/editor/m_editor'; + +export default Editor; + +/** + * @name EditorOptions.name + * @type string + * @default "" + * @hidden + */ diff --git a/packages/devextreme/js/ui/editor/ui.data_expression.js b/packages/devextreme/js/ui/editor/ui.data_expression.js new file mode 100644 index 000000000000..37e7d2198209 --- /dev/null +++ b/packages/devextreme/js/ui/editor/ui.data_expression.js @@ -0,0 +1,3 @@ +import DataExpressionMixin from '../../__internal/ui/editor/m_data_expression'; + +export default DataExpressionMixin; From c916b22eb430887ec43a7b6028f889b253d489a3 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 13:25:05 +0400 Subject: [PATCH 22/32] TextEditor, TextBox: move files to TS --- .../text_box/text_box.js => __internal/ui/text_box/m_text_box.ts} | 0 .../ui/text_box/m_text_editor.base.ts} | 0 .../ui/text_box/m_text_editor.label.ts} | 0 .../ui/text_box/m_text_editor.mask.rule.ts} | 0 .../ui/text_box/m_text_editor.mask.strategy.ts} | 0 .../ui/text_box/m_text_editor.mask.ts} | 0 .../ui.text_editor.js => __internal/ui/text_box/m_text_editor.ts} | 0 .../utils.caret.js => __internal/ui/text_box/m_utils.caret.ts} | 0 .../utils.scroll.js => __internal/ui/text_box/m_utils.scroll.ts} | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/text_box/text_box.js => __internal/ui/text_box/m_text_box.ts} (100%) rename packages/devextreme/js/{ui/text_box/ui.text_editor.base.js => __internal/ui/text_box/m_text_editor.base.ts} (100%) rename packages/devextreme/js/{ui/text_box/ui.text_editor.label.js => __internal/ui/text_box/m_text_editor.label.ts} (100%) rename packages/devextreme/js/{ui/text_box/ui.text_editor.mask.rule.js => __internal/ui/text_box/m_text_editor.mask.rule.ts} (100%) rename packages/devextreme/js/{ui/text_box/ui.text_editor.mask.strategy.js => __internal/ui/text_box/m_text_editor.mask.strategy.ts} (100%) rename packages/devextreme/js/{ui/text_box/ui.text_editor.mask.js => __internal/ui/text_box/m_text_editor.mask.ts} (100%) rename packages/devextreme/js/{ui/text_box/ui.text_editor.js => __internal/ui/text_box/m_text_editor.ts} (100%) rename packages/devextreme/js/{ui/text_box/utils.caret.js => __internal/ui/text_box/m_utils.caret.ts} (100%) rename packages/devextreme/js/{ui/text_box/utils.scroll.js => __internal/ui/text_box/m_utils.scroll.ts} (100%) diff --git a/packages/devextreme/js/ui/text_box/text_box.js b/packages/devextreme/js/__internal/ui/text_box/m_text_box.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/text_box.js rename to packages/devextreme/js/__internal/ui/text_box/m_text_box.ts diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.base.js b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/ui.text_editor.base.js rename to packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.label.js b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/ui.text_editor.label.js rename to packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.mask.rule.js b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.rule.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/ui.text_editor.mask.rule.js rename to packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.rule.ts diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.mask.strategy.js b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.strategy.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/ui.text_editor.mask.strategy.js rename to packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.strategy.ts diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.mask.js b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/ui.text_editor.mask.js rename to packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.ts diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.js b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/ui.text_editor.js rename to packages/devextreme/js/__internal/ui/text_box/m_text_editor.ts diff --git a/packages/devextreme/js/ui/text_box/utils.caret.js b/packages/devextreme/js/__internal/ui/text_box/m_utils.caret.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/utils.caret.js rename to packages/devextreme/js/__internal/ui/text_box/m_utils.caret.ts diff --git a/packages/devextreme/js/ui/text_box/utils.scroll.js b/packages/devextreme/js/__internal/ui/text_box/m_utils.scroll.ts similarity index 100% rename from packages/devextreme/js/ui/text_box/utils.scroll.js rename to packages/devextreme/js/__internal/ui/text_box/m_utils.scroll.ts From fba0d8e030025df8fe0b19523a620809510e5649 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 14:43:31 +0400 Subject: [PATCH 23/32] TextEditor, TextBox: ignore errors after move to TS --- .../devextreme/js/__internal/ui/m_tag_box.ts | 4 +- .../js/__internal/ui/m_text_area.ts | 2 +- .../js/__internal/ui/text_box/m_text_box.ts | 297 +-- .../ui/text_box/m_text_editor.base.ts | 1731 ++++++++--------- .../ui/text_box/m_text_editor.label.ts | 333 ++-- .../ui/text_box/m_text_editor.mask.rule.ts | 414 ++-- .../text_box/m_text_editor.mask.strategy.ts | 461 ++--- .../ui/text_box/m_text_editor.mask.ts | 1094 +++++------ .../__internal/ui/text_box/m_text_editor.ts | 5 +- .../__internal/ui/text_box/m_utils.caret.ts | 87 +- .../__internal/ui/text_box/m_utils.scroll.ts | 90 +- .../js/ui/html_editor/ui.html_editor.js | 2 +- .../devextreme/js/ui/text_box/text_box.js | 5 + .../js/ui/text_box/ui.text_editor.base.js | 3 + .../js/ui/text_box/ui.text_editor.js | 3 + .../lookup.tests.js | 2 +- .../tagBox.tests.js | 2 +- .../textEditorLabel.tests.js | 2 +- .../textEditorParts/caretWorkaround.js | 2 +- .../textEditorParts/common.tests.js | 2 +- .../textbox.tests.js | 2 +- .../DevExpress.utils/utils.caret.tests.js | 2 +- 22 files changed, 2306 insertions(+), 2239 deletions(-) create mode 100644 packages/devextreme/js/ui/text_box/text_box.js create mode 100644 packages/devextreme/js/ui/text_box/ui.text_editor.base.js create mode 100644 packages/devextreme/js/ui/text_box/ui.text_editor.js diff --git a/packages/devextreme/js/__internal/ui/m_tag_box.ts b/packages/devextreme/js/__internal/ui/m_tag_box.ts index 76a6f954a577..3ec7377138c2 100644 --- a/packages/devextreme/js/__internal/ui/m_tag_box.ts +++ b/packages/devextreme/js/__internal/ui/m_tag_box.ts @@ -22,9 +22,9 @@ import eventsEngine from '@js/events/core/events_engine'; import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '@js/events/utils/index'; import messageLocalization from '@js/localization/message'; import SelectBox from '@js/ui/select_box'; -import caret from '@js/ui/text_box/utils.caret'; -import { allowScroll } from '@js/ui/text_box/utils.scroll'; import errors from '@js/ui/widget/ui.errors'; +import caret from '@ts/ui/text_box/m_utils.caret'; +import { allowScroll } from '@ts/ui/text_box/m_utils.scroll'; function xor(a: boolean, b: boolean): boolean { return (a || b) && !(a && b); diff --git a/packages/devextreme/js/__internal/ui/m_text_area.ts b/packages/devextreme/js/__internal/ui/m_text_area.ts index 12726af36979..c44159027e0b 100644 --- a/packages/devextreme/js/__internal/ui/m_text_area.ts +++ b/packages/devextreme/js/__internal/ui/m_text_area.ts @@ -12,7 +12,7 @@ import scrollEvents from '@js/events/gesture/emitter.gesture.scroll'; import pointerEvents from '@js/events/pointer'; import { addNamespace, eventData } from '@js/events/utils/index'; import TextBox from '@js/ui/text_box'; -import { allowScroll, prepareScrollData } from '@js/ui/text_box/utils.scroll'; +import { allowScroll, prepareScrollData } from '@ts/ui/text_box/m_utils.scroll'; const TEXTAREA_CLASS = 'dx-textarea'; const TEXTEDITOR_INPUT_CLASS_AUTO_RESIZE = 'dx-texteditor-input-auto-resize'; diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_box.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_box.ts index 0605f6ccd346..4f6251a9fb85 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_box.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_box.ts @@ -1,13 +1,13 @@ -import $ from '../../core/renderer'; -import { getWindow } from '../../core/utils/window'; -const window = getWindow(); -import { extend } from '../../core/utils/extend'; -import registerComponent from '../../core/component_registrator'; -import TextEditor from './ui.text_editor'; -import { normalizeKeyName } from '../../events/utils/index'; -import { getOuterWidth, getWidth } from '../../core/utils/size'; +import registerComponent from '@js/core/component_registrator'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { getOuterWidth, getWidth } from '@js/core/utils/size'; +import { getWindow } from '@js/core/utils/window'; +import { normalizeKeyName } from '@js/events/utils/index'; + +import TextEditor from './m_text_editor'; -// STYLE textBox +const window = getWindow(); const ignoreKeys = ['backspace', 'tab', 'enter', 'pageUp', 'pageDown', 'end', 'home', 'leftArrow', 'rightArrow', 'downArrow', 'upArrow', 'del']; @@ -18,156 +18,157 @@ const SEARCH_ICON_CLASS = 'dx-icon-search'; const TextBox = TextEditor.inherit({ - ctor: function(element, options) { - if(options) { - this._showClearButton = options.showClearButton; - } + ctor(element, options) { + if (options) { + this._showClearButton = options.showClearButton; + } + + this.callBase.apply(this, arguments); + }, + + _getDefaultOptions() { + return extend(this.callBase(), { + value: '', + mode: 'text', + + maxLength: null, + }); + }, + + _initMarkup() { + this.$element().addClass(TEXTBOX_CLASS); - this.callBase.apply(this, arguments); - }, + this.callBase(); + this.setAria('role', 'textbox'); + }, - _getDefaultOptions: function() { - return extend(this.callBase(), { - value: '', - mode: 'text', + _renderInputType() { + this.callBase(); - maxLength: null - }); - }, + this._renderSearchMode(); + }, - _initMarkup: function() { - this.$element().addClass(TEXTBOX_CLASS); + _useTemplates() { + return false; + }, - this.callBase(); - this.setAria('role', 'textbox'); - }, + _renderProps() { + this.callBase(); + this._toggleMaxLengthProp(); + }, - _renderInputType: function() { - this.callBase(); + _toggleMaxLengthProp() { + const maxLength = this._getMaxLength(); + if (maxLength && maxLength > 0) { + this._input().attr('maxLength', maxLength); + } else { + this._input().removeAttr('maxLength'); + } + }, + + _renderSearchMode() { + const $element = this._$element; + + if (this.option('mode') === 'search') { + $element.addClass(SEARCHBOX_CLASS); + this._renderSearchIcon(); + + if (this._showClearButton === undefined) { + this._showClearButton = this.option('showClearButton'); + this.option('showClearButton', true); + } + } else { + $element.removeClass(SEARCHBOX_CLASS); + this._$searchIcon && this._$searchIcon.remove(); + this.option('showClearButton', this._showClearButton === undefined ? this.option('showClearButton') : this._showClearButton); + delete this._showClearButton; + } + }, + + _renderSearchIcon() { + const $searchIcon = $('
') + .addClass(ICON_CLASS) + .addClass(SEARCH_ICON_CLASS); + + $searchIcon.prependTo(this._input().parent()); + this._$searchIcon = $searchIcon; + }, + + _getLabelContainerWidth() { + if (this._$searchIcon) { + const $inputContainer = this._input().parent(); + + return getWidth($inputContainer) - this._getLabelBeforeWidth(); + } - this._renderSearchMode(); - }, + return this.callBase(); + }, - _useTemplates: function() { - return false; - }, + _getLabelBeforeWidth() { + let labelBeforeWidth = this.callBase(); - _renderProps: function() { - this.callBase(); + if (this._$searchIcon) { + labelBeforeWidth += getOuterWidth(this._$searchIcon); + } + + return labelBeforeWidth; + }, + + _optionChanged(args) { + switch (args.name) { + case 'maxLength': + this._toggleMaxLengthProp(); + break; + case 'mode': + this.callBase(args); + this._updateLabelWidth(); + break; + case 'mask': + this.callBase(args); this._toggleMaxLengthProp(); - }, - - _toggleMaxLengthProp: function() { - const maxLength = this._getMaxLength(); - if(maxLength && maxLength > 0) { - this._input().attr('maxLength', maxLength); - } else { - this._input().removeAttr('maxLength'); - } - }, - - _renderSearchMode: function() { - const $element = this._$element; - - if(this.option('mode') === 'search') { - $element.addClass(SEARCHBOX_CLASS); - this._renderSearchIcon(); - - if(this._showClearButton === undefined) { - this._showClearButton = this.option('showClearButton'); - this.option('showClearButton', true); - } - } else { - $element.removeClass(SEARCHBOX_CLASS); - this._$searchIcon && this._$searchIcon.remove(); - this.option('showClearButton', this._showClearButton === undefined ? this.option('showClearButton') : this._showClearButton); - delete this._showClearButton; - } - }, - - _renderSearchIcon: function() { - const $searchIcon = $('
') - .addClass(ICON_CLASS) - .addClass(SEARCH_ICON_CLASS); - - $searchIcon.prependTo(this._input().parent()); - this._$searchIcon = $searchIcon; - }, - - _getLabelContainerWidth: function() { - if(this._$searchIcon) { - const $inputContainer = this._input().parent(); - - return getWidth($inputContainer) - this._getLabelBeforeWidth(); - } - - return this.callBase(); - }, - - _getLabelBeforeWidth: function() { - let labelBeforeWidth = this.callBase(); - - if(this._$searchIcon) { - labelBeforeWidth += getOuterWidth(this._$searchIcon); - } - - return labelBeforeWidth; - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'maxLength': - this._toggleMaxLengthProp(); - break; - case 'mode': - this.callBase(args); - this._updateLabelWidth(); - break; - case 'mask': - this.callBase(args); - this._toggleMaxLengthProp(); - break; - default: - this.callBase(args); - } - }, - - _onKeyDownCutOffHandler: function(e) { - const actualMaxLength = this._getMaxLength(); - - if(actualMaxLength && !e.ctrlKey && !this._hasSelection()) { - const $input = $(e.target); - const key = normalizeKeyName(e); - - this._cutOffExtraChar($input); - - return ($input.val().length < actualMaxLength - || ignoreKeys.includes(key) - || window.getSelection().toString() !== ''); - } else { - return true; - } - }, - - _onChangeCutOffHandler: function(e) { - const $input = $(e.target); - if(this.option('maxLength')) { - this._cutOffExtraChar($input); - } - }, - - _cutOffExtraChar: function($input) { - const actualMaxLength = this._getMaxLength(); - const textInput = $input.val(); - if(actualMaxLength && textInput.length > actualMaxLength) { - $input.val(textInput.substr(0, actualMaxLength)); - } - }, - - _getMaxLength: function() { - const isMaskSpecified = !!this.option('mask'); - return isMaskSpecified ? null : this.option('maxLength'); + break; + default: + this.callBase(args); + } + }, + + _onKeyDownCutOffHandler(e) { + const actualMaxLength = this._getMaxLength(); + + if (actualMaxLength && !e.ctrlKey && !this._hasSelection()) { + const $input = $(e.target); + const key = normalizeKeyName(e); + + this._cutOffExtraChar($input); + + return $input.val().length < actualMaxLength + // @ts-expect-error + || ignoreKeys.includes(key) + // @ts-expect-error + || window.getSelection().toString() !== ''; + } + return true; + }, + + _onChangeCutOffHandler(e) { + const $input = $(e.target); + if (this.option('maxLength')) { + this._cutOffExtraChar($input); } + }, + + _cutOffExtraChar($input) { + const actualMaxLength = this._getMaxLength(); + const textInput = $input.val(); + if (actualMaxLength && textInput.length > actualMaxLength) { + $input.val(textInput.substr(0, actualMaxLength)); + } + }, + + _getMaxLength() { + const isMaskSpecified = !!this.option('mask'); + return isMaskSpecified ? null : this.option('maxLength'); + }, }); registerComponent('dxTextBox', TextBox); diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts index 4ad68061753e..a0501d25152d 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts @@ -1,30 +1,31 @@ -import $ from '../../core/renderer'; -import domAdapter from '../../core/dom_adapter'; -import eventsEngine from '../../events/core/events_engine'; -import { focused } from '../widget/selectors'; -import { isDefined } from '../../core/utils/type'; -import { extend } from '../../core/utils/extend'; -import { each } from '../../core/utils/iterator'; -import { current, isMaterial, isFluent } from '../themes'; -import devices from '../../core/devices'; -import Editor from '../editor/editor'; -import { addNamespace, normalizeKeyName } from '../../events/utils/index'; -import pointerEvents from '../../events/pointer'; -import ClearButton from '../../__internal/ui/text_box/m_text_editor.clear'; -import TextEditorButtonCollection from '../../__internal/ui/text_box/texteditor_button_collection/m_index'; -import config from '../../core/config'; -import errors from '../widget/ui.errors'; -import { Deferred } from '../../core/utils/deferred'; -import LoadIndicator from '../load_indicator'; -import { TextEditorLabel } from './ui.text_editor.label'; -import { getWidth } from '../../core/utils/size'; -import resizeObserverSingleton from '../../core/resize_observer'; -import Guid from '../../core/guid'; +import config from '@js/core/config'; +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import Guid from '@js/core/guid'; +import $ from '@js/core/renderer'; +import resizeObserverSingleton from '@js/core/resize_observer'; +import { Deferred } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { getWidth } from '@js/core/utils/size'; +import { isDefined } from '@js/core/utils/type'; +import eventsEngine from '@js/events/core/events_engine'; +import pointerEvents from '@js/events/pointer'; +import { addNamespace, normalizeKeyName } from '@js/events/utils/index'; +import Editor from '@js/ui/editor/editor'; +import LoadIndicator from '@js/ui/load_indicator'; +import { current, isFluent, isMaterial } from '@js/ui/themes'; +import { focused } from '@js/ui/widget/selectors'; +import errors from '@js/ui/widget/ui.errors'; + +import ClearButton from './m_text_editor.clear'; +import { TextEditorLabel } from './m_text_editor.label'; +import TextEditorButtonCollection from './texteditor_button_collection/m_index'; const TEXTEDITOR_CLASS = 'dx-texteditor'; const TEXTEDITOR_INPUT_CONTAINER_CLASS = 'dx-texteditor-input-container'; const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; -const TEXTEDITOR_INPUT_SELECTOR = '.' + TEXTEDITOR_INPUT_CLASS; +const TEXTEDITOR_INPUT_SELECTOR = `.${TEXTEDITOR_INPUT_CLASS}`; const TEXTEDITOR_CONTAINER_CLASS = 'dx-texteditor-container'; const TEXTEDITOR_BUTTONS_CONTAINER_CLASS = 'dx-texteditor-buttons-container'; const TEXTEDITOR_PLACEHOLDER_CLASS = 'dx-placeholder'; @@ -36,947 +37,931 @@ const TEXTEDITOR_VALIDATION_PENDING_CLASS = 'dx-validation-pending'; const TEXTEDITOR_VALID_CLASS = 'dx-valid'; const EVENTS_LIST = [ - 'KeyDown', 'KeyPress', 'KeyUp', - 'Change', 'Cut', 'Copy', 'Paste', 'Input' + 'KeyDown', 'KeyPress', 'KeyUp', + 'Change', 'Cut', 'Copy', 'Paste', 'Input', ]; const CONTROL_KEYS = [ - 'tab', - 'enter', - 'shift', - 'control', - 'alt', - 'escape', - 'pageUp', - 'pageDown', - 'end', - 'home', - 'leftArrow', - 'upArrow', - 'rightArrow', - 'downArrow', + 'tab', + 'enter', + 'shift', + 'control', + 'alt', + 'escape', + 'pageUp', + 'pageDown', + 'end', + 'home', + 'leftArrow', + 'upArrow', + 'rightArrow', + 'downArrow', ]; let TextEditorLabelCreator = TextEditorLabel; function checkButtonsOptionType(buttons) { - if(isDefined(buttons) && !Array.isArray(buttons)) { - throw errors.Error('E1053'); - } + if (isDefined(buttons) && !Array.isArray(buttons)) { + throw errors.Error('E1053'); + } } - +// @ts-expect-error const TextEditorBase = Editor.inherit({ - ctor: function(_, options) { - if(options) { - checkButtonsOptionType(options.buttons); - } - - this._buttonCollection = new TextEditorButtonCollection(this, this._getDefaultButtons()); - - this._$beforeButtonsContainer = null; - this._$afterButtonsContainer = null; - this._labelContainerElement = null; - - this.callBase.apply(this, arguments); - }, - - _getDefaultOptions: function() { - return extend(this.callBase(), { - buttons: void 0, - - value: '', - - spellcheck: false, - - showClearButton: false, - - valueChangeEvent: 'change', - - placeholder: '', - - inputAttr: {}, - - onFocusIn: null, - - onFocusOut: null, - - onKeyDown: null, - - onKeyUp: null, + ctor(_, options) { + if (options) { + checkButtonsOptionType(options.buttons); + } - onChange: null, + this._buttonCollection = new TextEditorButtonCollection(this, this._getDefaultButtons()); + + this._$beforeButtonsContainer = null; + this._$afterButtonsContainer = null; + this._labelContainerElement = null; + + this.callBase.apply(this, arguments); + }, + + _getDefaultOptions() { + return extend(this.callBase(), { + // eslint-disable-next-line no-void + buttons: void 0, + value: '', + spellcheck: false, + showClearButton: false, + valueChangeEvent: 'change', + placeholder: '', + inputAttr: {}, + onFocusIn: null, + onFocusOut: null, + onKeyDown: null, + onKeyUp: null, + onChange: null, + onInput: null, + onCut: null, + onCopy: null, + onPaste: null, + onEnterKey: null, + mode: 'text', + hoverStateEnabled: true, + focusStateEnabled: true, + text: undefined, + displayValueFormatter(value) { + return isDefined(value) && value !== false ? value : ''; + }, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + stylingMode: config().editorStylingMode || 'outlined', + showValidationMark: true, + label: '', + labelMode: 'static', + labelMark: '', + }); + }, + + _defaultOptionsRules() { + return this.callBase().concat([ + { + device() { + const themeName = current(); + return isMaterial(themeName); + }, + options: { + labelMode: 'floating', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + stylingMode: config().editorStylingMode || 'filled', + }, + }, + { + device() { + const themeName = current(); + return isFluent(themeName); + }, + options: { + labelMode: 'outside', + }, + }, + ]); + }, + + _getDefaultButtons() { + return [{ name: 'clear', Ctor: ClearButton }]; + }, + + _isClearButtonVisible() { + return this.option('showClearButton') && !this.option('readOnly'); + }, + + _input() { + return this.$element().find(TEXTEDITOR_INPUT_SELECTOR).first(); + }, + + _isFocused() { + return focused(this._input()) || this.callBase(); + }, + + _inputWrapper() { + return this.$element(); + }, + + _buttonsContainer() { + return this._inputWrapper().find(`.${TEXTEDITOR_BUTTONS_CONTAINER_CLASS}`).eq(0); + }, + + _isControlKey(key) { + return CONTROL_KEYS.includes(key); + }, + + _renderStylingMode() { + this.callBase(); + this._updateButtonsStyling(this.option('stylingMode')); + }, + + _initMarkup() { + this.$element() + .addClass(TEXTEDITOR_CLASS); + + this._renderInput(); + this._renderButtonContainers(); + this._renderStylingMode(); + this._renderInputType(); + this._renderPlaceholder(); + + this._renderProps(); + + this.callBase(); + + this._renderValue(); + + this._renderLabel(); + }, + + _render() { + this.callBase(); + + this._refreshValueChangeEvent(); + this._renderEvents(); + + this._renderEnterKeyAction(); + this._renderEmptinessEvent(); + }, + + _renderInput() { + this._$textEditorContainer = $('
') + .addClass(TEXTEDITOR_CONTAINER_CLASS) + .appendTo(this.$element()); + + this._$textEditorInputContainer = $('
') + .addClass(TEXTEDITOR_INPUT_CONTAINER_CLASS) + .appendTo(this._$textEditorContainer); + this._$textEditorInputContainer.append(this._createInput()); + }, + + _getInputContainer() { + return this._$textEditorInputContainer; + }, + + _renderPendingIndicator() { + this.$element().addClass(TEXTEDITOR_VALIDATION_PENDING_CLASS); + const $inputContainer = this._getInputContainer(); + const $indicatorElement = $('
') + .addClass(TEXTEDITOR_PENDING_INDICATOR_CLASS) + .appendTo($inputContainer); + this._pendingIndicator = this._createComponent($indicatorElement, LoadIndicator); + }, + + _disposePendingIndicator() { + if (!this._pendingIndicator) { + return; + } + this._pendingIndicator.dispose(); + this._pendingIndicator.$element().remove(); + this._pendingIndicator = null; + this.$element().removeClass(TEXTEDITOR_VALIDATION_PENDING_CLASS); + }, + + _renderValidationState() { + this.callBase(); + const isPending = this.option('validationStatus') === 'pending'; + + if (isPending) { + !this._pendingIndicator && this._renderPendingIndicator(); + this._showValidMark = false; + } else { + if (this.option('validationStatus') === 'invalid') { + this._showValidMark = false; + } + if (!this._showValidMark && this.option('showValidationMark') === true) { + this._showValidMark = this.option('validationStatus') === 'valid' && !!this._pendingIndicator; + } + this._disposePendingIndicator(); + } - onInput: null, + this._toggleValidMark(); + }, + + _getButtonsContainer() { + return this._$textEditorContainer; + }, + + _renderButtonContainers() { + const buttons = this.option('buttons'); + + const $buttonsContainer = this._getButtonsContainer(); + this._$beforeButtonsContainer = this._buttonCollection.renderBeforeButtons(buttons, $buttonsContainer); + this._$afterButtonsContainer = this._buttonCollection.renderAfterButtons(buttons, $buttonsContainer); + }, + + _cleanButtonContainers() { + this._$beforeButtonsContainer?.remove(); + this._$afterButtonsContainer?.remove(); + this._buttonCollection.clean(); + }, + + _clean() { + this._buttonCollection.clean(); + this._disposePendingIndicator(); + this._unobserveLabelContainerResize(); + this._$beforeButtonsContainer = null; + this._$afterButtonsContainer = null; + this._$textEditorContainer = null; + this.callBase(); + }, + + _createInput() { + const $input = $(''); + this._applyInputAttributes($input, this.option('inputAttr')); + return $input; + }, + + _setSubmitElementName(name) { + const inputAttrName = this.option('inputAttr.name'); + return this.callBase(name || inputAttrName || ''); + }, + + _applyInputAttributes($input, customAttributes) { + const inputAttributes = extend(this._getDefaultAttributes(), customAttributes); + $input + .attr(inputAttributes) + .addClass(TEXTEDITOR_INPUT_CLASS); + + this._setInputMinHeight($input); + }, + + _setInputMinHeight($input) { + $input.css('minHeight', this.option('height') ? '0' : ''); + }, + + _getPlaceholderAttr() { + const { + ios, + // @ts-expect-error + mac, + } = devices.real(); + const { placeholder } = this.option(); + + // WA to fix vAlign (T898735) + // https://bugs.webkit.org/show_bug.cgi?id=142968 + const value = placeholder || (ios || mac ? ' ' : null); + + return value; + }, + + _getDefaultAttributes() { + const defaultAttributes = { + autocomplete: 'off', + placeholder: this._getPlaceholderAttr(), + }; + + return defaultAttributes; + }, + + _updateButtons(names) { + this._buttonCollection.updateButtons(names); + }, + + _updateButtonsStyling(editorStylingMode) { + each(this.option('buttons'), (_, { options, name: buttonName }) => { + if (options && !options.stylingMode && this.option('visible')) { + const buttonInstance = this.getButton(buttonName); + buttonInstance.option && buttonInstance.option('stylingMode', editorStylingMode === 'underlined' ? 'text' : 'contained'); + } + }); + }, + + _renderValue() { + const renderInputPromise = this._renderInputValue(); + return renderInputPromise.promise(); + }, + + _renderInputValue(value) { + value = value ?? this.option('value'); + + let text = this.option('text'); + const displayValue = this.option('displayValue'); + const displayValueFormatter = this.option('displayValueFormatter'); + + if (displayValue !== undefined && value !== null) { + text = displayValueFormatter(displayValue); + } else if (!isDefined(text)) { + text = displayValueFormatter(value); + } - onCut: null, + this.option('text', text); - onCopy: null, + // fallback to empty string is required to support WebKit native date picker in some basic scenarios + // can not be covered by QUnit + if (this._input().val() !== (isDefined(text) ? text : '')) { + this._renderDisplayText(text); + } else { + this._toggleEmptinessEventHandler(); + } - onPaste: null, + return Deferred().resolve(); + }, - onEnterKey: null, + _renderDisplayText(text) { + this._input().val(text); + this._toggleEmptinessEventHandler(); + }, - mode: 'text', + _isValueValid() { + if (this._input().length) { + const { validity } = this._input().get(0); - hoverStateEnabled: true, + if (validity) { + return validity.valid; + } + } - focusStateEnabled: true, + return true; + }, + + _toggleEmptiness(isEmpty) { + this.$element().toggleClass(TEXTEDITOR_EMPTY_INPUT_CLASS, isEmpty); + this._togglePlaceholder(isEmpty); + }, + + _togglePlaceholder(isEmpty) { + this.$element() + .find(`.${TEXTEDITOR_PLACEHOLDER_CLASS}`) + .eq(0) + .toggleClass(STATE_INVISIBLE_CLASS, !isEmpty); + }, + + _renderProps() { + this._toggleReadOnlyState(); + this._toggleSpellcheckState(); + this._toggleTabIndex(); + }, + + _toggleDisabledState(value) { + this.callBase.apply(this, arguments); + + const $input = this._input(); + $input.prop('disabled', value); + }, + + _toggleTabIndex() { + const $input = this._input(); + const disabled = this.option('disabled'); + const focusStateEnabled = this.option('focusStateEnabled'); + + if (disabled || !focusStateEnabled) { + $input.attr('tabIndex', -1); + } else { + $input.removeAttr('tabIndex'); + } + }, - text: undefined, + _toggleReadOnlyState() { + this._input().prop('readOnly', this._readOnlyPropValue()); + this.callBase(); + }, - displayValueFormatter: function(value) { - return isDefined(value) && value !== false ? value : ''; - }, + _readOnlyPropValue() { + return this.option('readOnly'); + }, + _toggleSpellcheckState() { + this._input().prop('spellcheck', this.option('spellcheck')); + }, - stylingMode: config().editorStylingMode || 'outlined', + _unobserveLabelContainerResize() { + if (this._labelContainerElement) { + resizeObserverSingleton.unobserve(this._labelContainerElement); - showValidationMark: true, + this._labelContainerElement = null; + } + }, - label: '', + _getLabelContainer() { + return this._input(); + }, - labelMode: 'static', + _getLabelContainerWidth() { + return getWidth(this._getLabelContainer()); + }, - labelMark: '' - }); - }, - - _defaultOptionsRules: function() { - return this.callBase().concat([ - { - device: function() { - const themeName = current(); - return isMaterial(themeName); - }, - options: { - labelMode: 'floating', - stylingMode: config().editorStylingMode || 'filled', - } - }, - { - device: function() { - const themeName = current(); - return isFluent(themeName); - }, - options: { - labelMode: 'outside' - } - } - ]); - }, - - _getDefaultButtons: function() { - return [{ name: 'clear', Ctor: ClearButton }]; - }, - - _isClearButtonVisible: function() { - return this.option('showClearButton') && !this.option('readOnly'); - }, - - _input: function() { - return this.$element().find(TEXTEDITOR_INPUT_SELECTOR).first(); - }, - - _isFocused: function() { - return focused(this._input()) || this.callBase(); - }, - - _inputWrapper: function() { - return this.$element(); - }, - - _buttonsContainer: function() { - return this._inputWrapper().find('.' + TEXTEDITOR_BUTTONS_CONTAINER_CLASS).eq(0); - }, - - _isControlKey: function(key) { - return CONTROL_KEYS.indexOf(key) !== -1; - }, - - _renderStylingMode: function() { - this.callBase(); - this._updateButtonsStyling(this.option('stylingMode')); - }, + _getLabelBeforeWidth() { + const buttonsBeforeWidth = this._$beforeButtonsContainer && getWidth(this._$beforeButtonsContainer); - _initMarkup: function() { - this.$element() - .addClass(TEXTEDITOR_CLASS); + return buttonsBeforeWidth ?? 0; + }, - this._renderInput(); - this._renderButtonContainers(); - this._renderStylingMode(); - this._renderInputType(); - this._renderPlaceholder(); + _updateLabelWidth() { + this._label.updateBeforeWidth(this._getLabelBeforeWidth()); + this._label.updateMaxWidth(this._getLabelContainerWidth()); + }, - this._renderProps(); + _getFieldElement() { + return this._getLabelContainer(); + }, - this.callBase(); + _setFieldAria(force) { + const inputAttr = this.option('inputAttr'); + const ariaLabel = inputAttr?.['aria-label']; + const labelId = this._label?.getId(); - this._renderValue(); + const value = ariaLabel ? undefined : labelId; - this._renderLabel(); - }, + if (value || force) { + const aria = { + // eslint-disable-next-line spellcheck/spell-checker + labelledby: value, + label: ariaLabel, + }; + this.setAria(aria, this._getFieldElement()); + } + }, + + _renderLabel() { + this._unobserveLabelContainerResize(); + + this._labelContainerElement = $(this._getLabelContainer()).get(0); + + const { + label, labelMode, labelMark, rtlEnabled, + } = this.option(); + + const labelConfig = { + onClickHandler: () => { + this.focus(); + }, + onHoverHandler: (e) => { e.stopPropagation(); }, + onActiveHandler: (e) => { e.stopPropagation(); }, + $editor: this.$element(), + text: label, + mark: labelMark, + mode: labelMode, + rtlEnabled, + containsButtonsBefore: !!this._$beforeButtonsContainer, + getContainerWidth: () => this._getLabelContainerWidth(), + getBeforeWidth: () => this._getLabelBeforeWidth(), + }; + + this._label = new TextEditorLabelCreator(labelConfig); + + this._setFieldAria(); + + if (this._labelContainerElement) { // NOTE: element can be not in DOM yet in React and Vue + resizeObserverSingleton.observe(this._labelContainerElement, this._updateLabelWidth.bind(this)); + } + }, - _render: function() { - this.callBase(); + _renderPlaceholder() { + this._renderPlaceholderMarkup(); + this._attachPlaceholderEvents(); + }, - this._refreshValueChangeEvent(); - this._renderEvents(); + _renderPlaceholderMarkup() { + if (this._$placeholder) { + this._$placeholder.remove(); + this._$placeholder = null; + } - this._renderEnterKeyAction(); - this._renderEmptinessEvent(); - }, - - _renderInput: function() { - this._$textEditorContainer = $('
') - .addClass(TEXTEDITOR_CONTAINER_CLASS) - .appendTo(this.$element()); - - this._$textEditorInputContainer = $('
') - .addClass(TEXTEDITOR_INPUT_CONTAINER_CLASS) - .appendTo(this._$textEditorContainer); - this._$textEditorInputContainer.append(this._createInput()); - }, - - _getInputContainer() { - return this._$textEditorInputContainer; - }, - - _renderPendingIndicator: function() { - this.$element().addClass(TEXTEDITOR_VALIDATION_PENDING_CLASS); - const $inputContainer = this._getInputContainer(); - const $indicatorElement = $('
') - .addClass(TEXTEDITOR_PENDING_INDICATOR_CLASS) - .appendTo($inputContainer); - this._pendingIndicator = this._createComponent($indicatorElement, LoadIndicator); - }, - - _disposePendingIndicator: function() { - if(!this._pendingIndicator) { + const $input = this._input(); + const placeholder = this.option('placeholder'); + const placeholderAttributes = { + id: placeholder ? `dx-${new Guid()}` : undefined, + 'data-dx_placeholder': placeholder, + }; + + const $placeholder = this._$placeholder = $('
') + // @ts-expect-error + .attr(placeholderAttributes); + + $placeholder.insertAfter($input); + $placeholder.addClass(TEXTEDITOR_PLACEHOLDER_CLASS); + }, + + _attachPlaceholderEvents() { + const startEvent = addNamespace(pointerEvents.up, this.NAME); + + eventsEngine.on(this._$placeholder, startEvent, () => { + // @ts-expect-error + eventsEngine.trigger(this._input(), 'focus'); + }); + this._toggleEmptinessEventHandler(); + }, + + _placeholder() { + // @ts-expect-error + return this._$placeholder || $(); + }, + + _clearValueHandler(e) { + const $input = this._input(); + e.stopPropagation(); + + this._saveValueChangeEvent(e); + this._clearValue(); + // @ts-expect-error + !this._isFocused() && eventsEngine.trigger($input, 'focus'); + // @ts-expect-error + eventsEngine.trigger($input, 'input'); + }, + + _clearValue() { + this.clear(); + }, + + _renderEvents() { + const $input = this._input(); + + each(EVENTS_LIST, (_, event) => { + if (this.hasActionSubscription(`on${event}`)) { + const action = this._createActionByOption(`on${event}`, { excludeValidators: ['readOnly'] }); + + eventsEngine.on($input, addNamespace(event.toLowerCase(), this.NAME), (e) => { + if (this._disposed) { return; - } - this._pendingIndicator.dispose(); - this._pendingIndicator.$element().remove(); - this._pendingIndicator = null; - this.$element().removeClass(TEXTEDITOR_VALIDATION_PENDING_CLASS); - }, - - _renderValidationState: function() { - this.callBase(); - const isPending = this.option('validationStatus') === 'pending'; - - if(isPending) { - !this._pendingIndicator && this._renderPendingIndicator(); - this._showValidMark = false; - } else { - if(this.option('validationStatus') === 'invalid') { - this._showValidMark = false; - } - if(!this._showValidMark && this.option('showValidationMark') === true) { - this._showValidMark = this.option('validationStatus') === 'valid' && !!this._pendingIndicator; - } - this._disposePendingIndicator(); - } + } - this._toggleValidMark(); - }, - - _getButtonsContainer() { - return this._$textEditorContainer; - }, - - _renderButtonContainers: function() { - const buttons = this.option('buttons'); - - const $buttonsContainer = this._getButtonsContainer(); - this._$beforeButtonsContainer = this._buttonCollection.renderBeforeButtons(buttons, $buttonsContainer); - this._$afterButtonsContainer = this._buttonCollection.renderAfterButtons(buttons, $buttonsContainer); - }, - - _cleanButtonContainers: function() { - this._$beforeButtonsContainer?.remove(); - this._$afterButtonsContainer?.remove(); - this._buttonCollection.clean(); - }, - - _clean() { - this._buttonCollection.clean(); - this._disposePendingIndicator(); - this._unobserveLabelContainerResize(); - this._$beforeButtonsContainer = null; - this._$afterButtonsContainer = null; - this._$textEditorContainer = null; - this.callBase(); - }, - - _createInput: function() { - const $input = $(''); - this._applyInputAttributes($input, this.option('inputAttr')); - return $input; - }, - - _setSubmitElementName: function(name) { - const inputAttrName = this.option('inputAttr.name'); - return this.callBase(name || inputAttrName || ''); - }, - - _applyInputAttributes: function($input, customAttributes) { - const inputAttributes = extend(this._getDefaultAttributes(), customAttributes); - $input - .attr(inputAttributes) - .addClass(TEXTEDITOR_INPUT_CLASS); - - this._setInputMinHeight($input); - }, - - _setInputMinHeight: function($input) { - $input.css('minHeight', this.option('height') ? '0' : ''); - }, - - _getPlaceholderAttr() { - const { ios, mac } = devices.real(); - const { placeholder } = this.option(); - - // WA to fix vAlign (T898735) - // https://bugs.webkit.org/show_bug.cgi?id=142968 - const value = placeholder || ((ios || mac) ? ' ' : null); - - return value; - }, - - _getDefaultAttributes() { - const defaultAttributes = { - autocomplete: 'off', - placeholder: this._getPlaceholderAttr(), - }; - - return defaultAttributes; - }, - - _updateButtons: function(names) { - this._buttonCollection.updateButtons(names); - }, - - _updateButtonsStyling: function(editorStylingMode) { - each(this.option('buttons'), (_, { options, name: buttonName }) => { - if(options && !options.stylingMode && this.option('visible')) { - const buttonInstance = this.getButton(buttonName); - buttonInstance.option && buttonInstance.option('stylingMode', editorStylingMode === 'underlined' ? 'text' : 'contained'); - } + action({ event: e }); }); - }, - - _renderValue: function() { - const renderInputPromise = this._renderInputValue(); - return renderInputPromise.promise(); - }, - - _renderInputValue: function(value) { - value = value ?? this.option('value'); - - let text = this.option('text'); - const displayValue = this.option('displayValue'); - const displayValueFormatter = this.option('displayValueFormatter'); + } + }); + }, - if(displayValue !== undefined && value !== null) { - text = displayValueFormatter(displayValue); - } else if(!isDefined(text)) { - text = displayValueFormatter(value); - } + _refreshEvents() { + const $input = this._input(); - this.option('text', text); + each(EVENTS_LIST, (_, event) => { + eventsEngine.off($input, addNamespace(event.toLowerCase(), this.NAME)); + }); - // fallback to empty string is required to support WebKit native date picker in some basic scenarios - // can not be covered by QUnit - if(this._input().val() !== (isDefined(text) ? text : '')) { - this._renderDisplayText(text); - } else { - this._toggleEmptinessEventHandler(); - } + this._renderEvents(); + }, - return new Deferred().resolve(); - }, + _keyPressHandler() { + this.option('text', this._input().val()); + }, - _renderDisplayText: function(text) { - this._input().val(text); - this._toggleEmptinessEventHandler(); - }, + _keyDownHandler(e) { + const $input = this._input(); + const isCtrlEnter = e.ctrlKey && normalizeKeyName(e) === 'enter'; + const isNewValue = $input.val() !== this.option('value'); - _isValueValid: function() { - if(this._input().length) { - const validity = this._input().get(0).validity; + if (isCtrlEnter && isNewValue) { + // @ts-expect-error + eventsEngine.trigger($input, 'change'); + } + }, + + _getValueChangeEventOptionName() { + return 'valueChangeEvent'; + }, + + _renderValueChangeEvent() { + const keyPressEvent = addNamespace(this._renderValueEventName(), `${this.NAME}TextChange`); + const valueChangeEvent = addNamespace(this.option(this._getValueChangeEventOptionName()), `${this.NAME}ValueChange`); + const keyDownEvent = addNamespace('keydown', `${this.NAME}TextChange`); + const $input = this._input(); + + eventsEngine.on($input, keyPressEvent, this._keyPressHandler.bind(this)); + eventsEngine.on($input, valueChangeEvent, this._valueChangeEventHandler.bind(this)); + eventsEngine.on($input, keyDownEvent, this._keyDownHandler.bind(this)); + }, + + _cleanValueChangeEvent() { + const valueChangeNamespace = `.${this.NAME}ValueChange`; + const textChangeNamespace = `.${this.NAME}TextChange`; + + eventsEngine.off(this._input(), valueChangeNamespace); + eventsEngine.off(this._input(), textChangeNamespace); + }, + + _refreshValueChangeEvent() { + this._cleanValueChangeEvent(); + this._renderValueChangeEvent(); + }, + + _renderValueEventName() { + return 'input change keypress'; + }, + + _focusTarget() { + return this._input(); + }, + + _focusEventTarget() { + return this.element(); + }, + + _isInput(element) { + return element === this._input().get(0); + }, + + _preventNestedFocusEvent(event) { + if (event.isDefaultPrevented()) { + return true; + } - if(validity) { - return validity.valid; - } - } + let shouldPrevent = this._isNestedTarget(event.relatedTarget); - return true; - }, + if (event.type === 'focusin') { + shouldPrevent = shouldPrevent && this._isNestedTarget(event.target) && !this._isInput(event.target); + } else if (!shouldPrevent) { + this._toggleFocusClass(false, this.$element()); + } - _toggleEmptiness: function(isEmpty) { - this.$element().toggleClass(TEXTEDITOR_EMPTY_INPUT_CLASS, isEmpty); - this._togglePlaceholder(isEmpty); - }, + shouldPrevent && event.preventDefault(); + return shouldPrevent; + }, - _togglePlaceholder: function(isEmpty) { - this.$element() - .find(`.${TEXTEDITOR_PLACEHOLDER_CLASS}`) - .eq(0) - .toggleClass(STATE_INVISIBLE_CLASS, !isEmpty); - }, + _isNestedTarget(target) { + return !!this.$element().find(target).length; + }, - _renderProps: function() { - this._toggleReadOnlyState(); - this._toggleSpellcheckState(); - this._toggleTabIndex(); - }, + _focusClassTarget() { + return this.$element(); + }, - _toggleDisabledState: function(value) { - this.callBase.apply(this, arguments); + _focusInHandler(event) { + this._preventNestedFocusEvent(event); - const $input = this._input(); - $input.prop('disabled', value); - }, + this.callBase.apply(this, arguments); + }, - _toggleTabIndex: function() { - const $input = this._input(); - const disabled = this.option('disabled'); - const focusStateEnabled = this.option('focusStateEnabled'); + _focusOutHandler(event) { + this._preventNestedFocusEvent(event); - if(disabled || !focusStateEnabled) { - $input.attr('tabIndex', -1); - } else { - $input.removeAttr('tabIndex'); - } - }, + this.callBase.apply(this, arguments); + }, - _toggleReadOnlyState: function() { - this._input().prop('readOnly', this._readOnlyPropValue()); - this.callBase(); - }, + _toggleFocusClass(isFocused, $element) { + this.callBase(isFocused, this._focusClassTarget($element)); + }, - _readOnlyPropValue: function() { - return this.option('readOnly'); - }, + _hasFocusClass(element) { + return this.callBase($(element || this.$element())); + }, - _toggleSpellcheckState: function() { - this._input().prop('spellcheck', this.option('spellcheck')); - }, + _renderEmptinessEvent() { + const $input = this._input(); - _unobserveLabelContainerResize: function() { - if(this._labelContainerElement) { - resizeObserverSingleton.unobserve(this._labelContainerElement); + eventsEngine.on($input, 'input blur', this._toggleEmptinessEventHandler.bind(this)); + }, - this._labelContainerElement = null; - } - }, + _toggleEmptinessEventHandler() { + const text = this._input().val(); + const isEmpty = (text === '' || text === null) && this._isValueValid(); - _getLabelContainer: function() { - return this._input(); - }, + this._toggleEmptiness(isEmpty); + }, - _getLabelContainerWidth: function() { - return getWidth(this._getLabelContainer()); - }, + _valueChangeEventHandler(e, formattedValue) { + if (this.option('readOnly')) { + return; + } + this._saveValueChangeEvent(e); + this.option('value', arguments.length > 1 ? formattedValue : this._input().val()); + this._saveValueChangeEvent(undefined); + }, + + _renderEnterKeyAction() { + this._enterKeyAction = this._createActionByOption('onEnterKey', { + excludeValidators: ['readOnly'], + }); + + eventsEngine.off(this._input(), 'keyup.onEnterKey.dxTextEditor'); + eventsEngine.on(this._input(), 'keyup.onEnterKey.dxTextEditor', this._enterKeyHandlerUp.bind(this)); + }, + + _enterKeyHandlerUp(e) { + if (this._disposed) { + return; + } - _getLabelBeforeWidth: function() { - const buttonsBeforeWidth = this._$beforeButtonsContainer && getWidth(this._$beforeButtonsContainer); + if (normalizeKeyName(e) === 'enter') { + this._enterKeyAction({ event: e }); + } + }, - return buttonsBeforeWidth ?? 0; - }, + _updateValue() { + this._options.silent('text', null); + this._renderValue(); + }, - _updateLabelWidth: function() { - this._label.updateBeforeWidth(this._getLabelBeforeWidth()); - this._label.updateMaxWidth(this._getLabelContainerWidth()); - }, + _dispose() { + this._enterKeyAction = undefined; + this.callBase(); + }, - _getFieldElement() { - return this._getLabelContainer(); - }, + _getSubmitElement() { + return this._input(); + }, - _setFieldAria(force) { - const inputAttr = this.option('inputAttr'); - const ariaLabel = inputAttr?.['aria-label']; - const labelId = this._label?.getId(); + _hasActiveElement() { + return this._input().is(domAdapter.getActiveElement(this._input()[0])); + }, - const value = ariaLabel ? undefined : labelId; + _optionChanged(args) { + const { name, fullName, value } = args; - if(value || force) { - const aria = { - 'labelledby': value, - label: ariaLabel, - }; - this.setAria(aria, this._getFieldElement()); - } - }, - - _renderLabel: function() { - this._unobserveLabelContainerResize(); - - this._labelContainerElement = $(this._getLabelContainer()).get(0); - - const { label, labelMode, labelMark, rtlEnabled } = this.option(); - - const labelConfig = { - onClickHandler: () => { - this.focus(); - }, - onHoverHandler: (e) => { e.stopPropagation(); }, - onActiveHandler: (e) => { e.stopPropagation(); }, - $editor: this.$element(), - text: label, - mark: labelMark, - mode: labelMode, - rtlEnabled, - containsButtonsBefore: !!this._$beforeButtonsContainer, - getContainerWidth: () => { - return this._getLabelContainerWidth(); - }, - getBeforeWidth: () => { - return this._getLabelBeforeWidth(); - } - }; - - this._label = new TextEditorLabelCreator(labelConfig); + const eventName = name.replace('on', ''); + if (EVENTS_LIST.includes(eventName)) { + this._refreshEvents(); + return; + } + switch (name) { + case 'valueChangeEvent': + this._refreshValueChangeEvent(); + this._refreshFocusEvent(); + this._refreshEvents(); + break; + case 'onValueChanged': + this._createValueChangeAction(); + break; + case 'focusStateEnabled': + this.callBase(args); + this._toggleTabIndex(); + break; + case 'spellcheck': + this._toggleSpellcheckState(); + break; + case 'mode': + this._renderInputType(); + break; + case 'onEnterKey': + this._renderEnterKeyAction(); + break; + case 'placeholder': + this._renderPlaceholder(); + this._setFieldAria(true); + this._input().attr({ placeholder: this._getPlaceholderAttr() }); + break; + case 'label': + this._label.updateText(value); + this._setFieldAria(true); + break; + case 'labelMark': + this._label.updateMark(value); + break; + case 'labelMode': + this._label.updateMode(value); this._setFieldAria(); - - if(this._labelContainerElement) { // NOTE: element can be not in DOM yet in React and Vue - resizeObserverSingleton.observe(this._labelContainerElement, this._updateLabelWidth.bind(this)); - } - }, - - _renderPlaceholder: function() { - this._renderPlaceholderMarkup(); - this._attachPlaceholderEvents(); - }, - - _renderPlaceholderMarkup: function() { - if(this._$placeholder) { - this._$placeholder.remove(); - this._$placeholder = null; - } - - const $input = this._input(); - const placeholder = this.option('placeholder'); - const placeholderAttributes = { - 'id': placeholder ? `dx-${new Guid()}` : undefined, - 'data-dx_placeholder': placeholder, - }; - - const $placeholder = this._$placeholder = $('
') - .attr(placeholderAttributes); - - $placeholder.insertAfter($input); - $placeholder.addClass(TEXTEDITOR_PLACEHOLDER_CLASS); - }, - - _attachPlaceholderEvents: function() { - const startEvent = addNamespace(pointerEvents.up, this.NAME); - - eventsEngine.on(this._$placeholder, startEvent, () => { - eventsEngine.trigger(this._input(), 'focus'); - }); - this._toggleEmptinessEventHandler(); - }, - - _placeholder: function() { - return this._$placeholder || $(); - }, - - _clearValueHandler: function(e) { - const $input = this._input(); - e.stopPropagation(); - - this._saveValueChangeEvent(e); - this._clearValue(); - - !this._isFocused() && eventsEngine.trigger($input, 'focus'); - eventsEngine.trigger($input, 'input'); - }, - - _clearValue: function() { - this.clear(); - }, - - _renderEvents: function() { - const $input = this._input(); - - each(EVENTS_LIST, (_, event) => { - if(this.hasActionSubscription('on' + event)) { - - const action = this._createActionByOption('on' + event, { excludeValidators: ['readOnly'] }); - - eventsEngine.on($input, addNamespace(event.toLowerCase(), this.NAME), (e) => { - if(this._disposed) { - return; - } - - action({ event: e }); - }); - } - }); - }, - - _refreshEvents: function() { - const $input = this._input(); - - each(EVENTS_LIST, (_, event) => { - eventsEngine.off($input, addNamespace(event.toLowerCase(), this.NAME)); - }); - - this._renderEvents(); - }, - - _keyPressHandler: function() { - this.option('text', this._input().val()); - }, - - _keyDownHandler: function(e) { - const $input = this._input(); - const isCtrlEnter = e.ctrlKey && normalizeKeyName(e) === 'enter'; - const isNewValue = $input.val() !== this.option('value'); - - if(isCtrlEnter && isNewValue) { - eventsEngine.trigger($input, 'change'); - } - }, - - _getValueChangeEventOptionName: function() { - return 'valueChangeEvent'; - }, - - _renderValueChangeEvent: function() { - const keyPressEvent = addNamespace(this._renderValueEventName(), `${this.NAME}TextChange`); - const valueChangeEvent = addNamespace(this.option(this._getValueChangeEventOptionName()), `${this.NAME}ValueChange`); - const keyDownEvent = addNamespace('keydown', `${this.NAME}TextChange`); - const $input = this._input(); - - eventsEngine.on($input, keyPressEvent, this._keyPressHandler.bind(this)); - eventsEngine.on($input, valueChangeEvent, this._valueChangeEventHandler.bind(this)); - eventsEngine.on($input, keyDownEvent, this._keyDownHandler.bind(this)); - }, - - _cleanValueChangeEvent: function() { - const valueChangeNamespace = `.${this.NAME}ValueChange`; - const textChangeNamespace = `.${this.NAME}TextChange`; - - eventsEngine.off(this._input(), valueChangeNamespace); - eventsEngine.off(this._input(), textChangeNamespace); - }, - - _refreshValueChangeEvent: function() { - this._cleanValueChangeEvent(); - this._renderValueChangeEvent(); - }, - - _renderValueEventName: function() { - return 'input change keypress'; - }, - - _focusTarget: function() { - return this._input(); - }, - - _focusEventTarget: function() { - return this.element(); - }, - - _isInput: function(element) { - return element === this._input().get(0); - }, - - _preventNestedFocusEvent: function(event) { - if(event.isDefaultPrevented()) { - return true; - } - - let shouldPrevent = this._isNestedTarget(event.relatedTarget); - - if(event.type === 'focusin') { - shouldPrevent = shouldPrevent && this._isNestedTarget(event.target) && !this._isInput(event.target); - } else if(!shouldPrevent) { - this._toggleFocusClass(false, this.$element()); - } - - shouldPrevent && event.preventDefault(); - return shouldPrevent; - }, - - _isNestedTarget: function(target) { - return !!this.$element().find(target).length; - }, - - _focusClassTarget: function() { - return this.$element(); - }, - - _focusInHandler: function(event) { - this._preventNestedFocusEvent(event); - - this.callBase.apply(this, arguments); - }, - - _focusOutHandler: function(event) { - this._preventNestedFocusEvent(event); - - this.callBase.apply(this, arguments); - }, - - _toggleFocusClass: function(isFocused, $element) { - this.callBase(isFocused, this._focusClassTarget($element)); - }, - - _hasFocusClass: function(element) { - return this.callBase($(element || this.$element())); - }, - - _renderEmptinessEvent: function() { - const $input = this._input(); - - eventsEngine.on($input, 'input blur', this._toggleEmptinessEventHandler.bind(this)); - }, - - _toggleEmptinessEventHandler: function() { - const text = this._input().val(); - const isEmpty = (text === '' || text === null) && this._isValueValid(); - - this._toggleEmptiness(isEmpty); - }, - - _valueChangeEventHandler: function(e, formattedValue) { - if(this.option('readOnly')) { - return; - } - this._saveValueChangeEvent(e); - this.option('value', arguments.length > 1 ? formattedValue : this._input().val()); - this._saveValueChangeEvent(undefined); - }, - - _renderEnterKeyAction: function() { - this._enterKeyAction = this._createActionByOption('onEnterKey', { - excludeValidators: ['readOnly'] - }); - - eventsEngine.off(this._input(), 'keyup.onEnterKey.dxTextEditor'); - eventsEngine.on(this._input(), 'keyup.onEnterKey.dxTextEditor', this._enterKeyHandlerUp.bind(this)); - }, - - _enterKeyHandlerUp: function(e) { - if(this._disposed) { - return; + break; + case 'width': + this.callBase(args); + this._label.updateMaxWidth(this._getLabelContainerWidth()); + break; + case 'readOnly': + case 'disabled': + this._updateButtons(); + this.callBase(args); + break; + case 'showClearButton': + this._updateButtons(['clear']); + break; + case 'text': + break; + case 'value': + this._updateValue(); + this.callBase(args); + break; + case 'inputAttr': + this._applyInputAttributes(this._input(), this.option(name)); + break; + case 'stylingMode': + this._renderStylingMode(); + this._updateLabelWidth(); + break; + case 'buttons': + if (fullName === name) { + checkButtonsOptionType(value); } - - if(normalizeKeyName(e) === 'enter') { - this._enterKeyAction({ event: e }); + this._cleanButtonContainers(); + this._renderButtonContainers(); + this._updateButtonsStyling(this.option('stylingMode')); + this._updateLabelWidth(); + this._label.updateContainsButtonsBefore(!!this._$beforeButtonsContainer); + break; + case 'visible': + this.callBase(args); + if (value && this.option('buttons')) { + this._cleanButtonContainers(); + this._renderButtonContainers(); + this._updateButtonsStyling(this.option('stylingMode')); } - }, - - _updateValue: function() { - this._options.silent('text', null); - this._renderValue(); - }, - - _dispose: function() { - this._enterKeyAction = undefined; - this.callBase(); - }, - - _getSubmitElement: function() { - return this._input(); - }, - - _hasActiveElement: function() { - return this._input().is(domAdapter.getActiveElement(this._input()[0])); - }, - - _optionChanged: function(args) { - const { name, fullName, value } = args; + break; + case 'displayValueFormatter': + this._invalidate(); + break; + case 'showValidationMark': + break; + default: + this.callBase(args); + } + }, - const eventName = name.replace('on', ''); - if(EVENTS_LIST.includes(eventName)) { - this._refreshEvents(); - return; - } + _renderInputType() { + // B218621, B231875 + this._setInputType(this.option('mode')); + }, - switch(name) { - case 'valueChangeEvent': - this._refreshValueChangeEvent(); - this._refreshFocusEvent(); - this._refreshEvents(); - break; - case 'onValueChanged': - this._createValueChangeAction(); - break; - case 'focusStateEnabled': - this.callBase(args); - this._toggleTabIndex(); - break; - case 'spellcheck': - this._toggleSpellcheckState(); - break; - case 'mode': - this._renderInputType(); - break; - case 'onEnterKey': - this._renderEnterKeyAction(); - break; - case 'placeholder': - this._renderPlaceholder(); - this._setFieldAria(true); - this._input().attr({ placeholder: this._getPlaceholderAttr() }); - break; - case 'label': - this._label.updateText(value); - this._setFieldAria(true); - break; - case 'labelMark': - this._label.updateMark(value); - break; - case 'labelMode': - this._label.updateMode(value); - this._setFieldAria(); - break; - case 'width': - this.callBase(args); - this._label.updateMaxWidth(this._getLabelContainerWidth()); - break; - case 'readOnly': - case 'disabled': - this._updateButtons(); - this.callBase(args); - break; - case 'showClearButton': - this._updateButtons(['clear']); - break; - case 'text': - break; - case 'value': - this._updateValue(); - this.callBase(args); - break; - case 'inputAttr': - this._applyInputAttributes(this._input(), this.option(name)); - break; - case 'stylingMode': - this._renderStylingMode(); - this._updateLabelWidth(); - break; - case 'buttons': - if(fullName === name) { - checkButtonsOptionType(value); - } - this._cleanButtonContainers(); - this._renderButtonContainers(); - this._updateButtonsStyling(this.option('stylingMode')); - this._updateLabelWidth(); - this._label.updateContainsButtonsBefore(!!this._$beforeButtonsContainer); - break; - case 'visible': - this.callBase(args); - if(value && this.option('buttons')) { - this._cleanButtonContainers(); - this._renderButtonContainers(); - this._updateButtonsStyling(this.option('stylingMode')); - } - break; - case 'displayValueFormatter': - this._invalidate(); - break; - case 'showValidationMark': - break; - default: - this.callBase(args); - } - }, + _setInputType(type) { + const input = this._input(); - _renderInputType: function() { - // B218621, B231875 - this._setInputType(this.option('mode')); - }, + if (type === 'search') { + type = 'text'; + } - _setInputType: function(type) { - const input = this._input(); + try { + input.prop('type', type); + } catch (e) { + input.prop('type', 'text'); + } + }, - if(type === 'search') { - type = 'text'; - } + getButton(name) { + return this._buttonCollection.getButton(name); + }, - try { - input.prop('type', type); - } catch(e) { - input.prop('type', 'text'); - } - }, + focus() { + // @ts-expect-error + eventsEngine.trigger(this._input(), 'focus'); + }, - getButton(name) { - return this._buttonCollection.getButton(name); - }, + clear() { + if (this._showValidMark) { + this._showValidMark = false; + this._renderValidationState(); + } - focus: function() { - eventsEngine.trigger(this._input(), 'focus'); - }, + const defaultOptions = this._getDefaultOptions(); + if (this.option('value') === defaultOptions.value) { + this._options.silent('text', ''); + this._renderValue(); + } else { + this.option('value', defaultOptions.value); + } + }, + + _resetToInitialValue() { + if (this.option('value') === this._initialValue) { + this._options.silent('text', this._initialValue); + this._renderValue(); + } else { + this.callBase(); + } - clear: function() { - if(this._showValidMark) { - this._showValidMark = false; - this._renderValidationState(); - } + this._disposePendingIndicator(); + this._showValidMark = false; + this._toggleValidMark(); + }, - const defaultOptions = this._getDefaultOptions(); - if(this.option('value') === defaultOptions.value) { - this._options.silent('text', ''); - this._renderValue(); - } else { - this.option('value', defaultOptions.value); - } - }, - - _resetToInitialValue() { - if(this.option('value') === this._initialValue) { - this._options.silent('text', this._initialValue); - this._renderValue(); - } else { - this.callBase(); - } + _toggleValidMark() { + this.$element().toggleClass(TEXTEDITOR_VALID_CLASS, !!this._showValidMark); + }, - this._disposePendingIndicator(); - this._showValidMark = false; - this._toggleValidMark(); - }, - - _toggleValidMark() { - this.$element().toggleClass(TEXTEDITOR_VALID_CLASS, !!this._showValidMark); - }, - - reset: function(value = undefined) { - if(arguments.length) { - this.callBase(value); - } else { - this.callBase(); - } - }, + reset(value = undefined) { + if (arguments.length) { + this.callBase(value); + } else { + this.callBase(); + } + }, - on: function(eventName, eventHandler) { - const result = this.callBase(eventName, eventHandler); - const event = eventName.charAt(0).toUpperCase() + eventName.substr(1); + on(eventName, eventHandler) { + const result = this.callBase(eventName, eventHandler); + const event = eventName.charAt(0).toUpperCase() + eventName.substr(1); - if(EVENTS_LIST.indexOf(event) >= 0) { - this._refreshEvents(); - } - return result; + if (EVENTS_LIST.includes(event)) { + this._refreshEvents(); } + return result; + }, }); - -///#DEBUG +/// #DEBUG TextEditorBase.mockTextEditorLabel = (mock) => { - TextEditorLabelCreator = mock; + TextEditorLabelCreator = mock; }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars TextEditorBase.restoreTextEditorLabel = (mock) => { - TextEditorLabelCreator = TextEditorLabel; + TextEditorLabelCreator = TextEditorLabel; }; -///#ENDDEBUG +/// #ENDDEBUG export default TextEditorBase; diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts index 1b7c3369f01a..b654a2da7dc2 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts @@ -1,12 +1,13 @@ -import $ from '../../core/renderer'; -import Guid from '../../core/guid'; -import { name as click } from '../../events/click'; -import eventsEngine from '../../events/core/events_engine'; -import { addNamespace } from '../../events/utils/index'; -import { start as hoverStart } from '../../events/hover'; -import { active } from '../../events/core/emitter.feedback'; -import { getWindow } from '../../core/utils/window'; -import { getWidth } from '../../core/utils/size'; +import Guid from '@js/core/guid'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { getWidth } from '@js/core/utils/size'; +import { getWindow } from '@js/core/utils/window'; +import { name as click } from '@js/events/click'; +import { active } from '@js/events/core/emitter.feedback'; +import eventsEngine from '@js/events/core/events_engine'; +import { start as hoverStart } from '@js/events/hover'; +import { addNamespace } from '@js/events/utils/index'; const TEXTEDITOR_LABEL_CLASS = 'dx-texteditor-label'; const TEXTEDITOR_WITH_LABEL_CLASS = 'dx-texteditor-with-label'; @@ -19,203 +20,221 @@ const LABEL_CLASS = 'dx-label'; const LABEL_AFTER_CLASS = 'dx-label-after'; class TextEditorLabel { - constructor(props) { - this.NAME = 'dxLabel'; - this._props = props; + public NAME: string; - this._id = `${TEXTEDITOR_LABEL_CLASS}-${new Guid()}`; + _props: any; - this._render(); - this._toggleMarkupVisibility(); - } + _id: string; - _isVisible() { - return !!this._props.text && this._props.mode !== 'hidden'; - } + _$before?: dxElementWrapper; - _render() { - this._$before = $('
').addClass(LABEL_BEFORE_CLASS); + _$labelSpan!: dxElementWrapper; - this._$labelSpan = $(''); - this._$label = $('
') - .addClass(LABEL_CLASS) - .append(this._$labelSpan); + _$label!: dxElementWrapper; - this._$after = $('
').addClass(LABEL_AFTER_CLASS); + _$after?: dxElementWrapper; - this._$root = $('
') - .addClass(TEXTEDITOR_LABEL_CLASS) - .attr('id', this._id) - .append(this._$before) - .append(this._$label) - .append(this._$after); + _$root!: dxElementWrapper; - this._updateMark(); - this._updateText(); - this._updateBeforeWidth(); - this._updateMaxWidth(); - } + constructor(props) { + this.NAME = 'dxLabel'; + this._props = props; - _toggleMarkupVisibility() { - const visible = this._isVisible(); + this._id = `${TEXTEDITOR_LABEL_CLASS}-${new Guid()}`; - this._updateEditorBeforeButtonsClass(visible); - this._updateEditorLabelClass(visible); + this._render(); + this._toggleMarkupVisibility(); + } - visible - ? this._$root.appendTo(this._props.$editor) - : this._$root.detach(); + _isVisible() { + return !!this._props.text && this._props.mode !== 'hidden'; + } - this._attachEvents(); - } + _render() { + this._$before = $('
').addClass(LABEL_BEFORE_CLASS); - _attachEvents() { - const clickEventName = addNamespace(click, this.NAME); - const hoverStartEventName = addNamespace(hoverStart, this.NAME); - const activeEventName = addNamespace(active, this.NAME); - - eventsEngine.off(this._$labelSpan, clickEventName); - eventsEngine.off(this._$labelSpan, hoverStartEventName); - eventsEngine.off(this._$labelSpan, activeEventName); - - if(this._isVisible() && this._isOutsideMode()) { - eventsEngine.on(this._$labelSpan, clickEventName, (e) => { - const selectedText = getWindow().getSelection().toString(); - - if(selectedText === '') { - this._props.onClickHandler(); - e.preventDefault(); - } - }); - eventsEngine.on(this._$labelSpan, hoverStartEventName, (e) => { - this._props.onHoverHandler(e); - }); - eventsEngine.on(this._$labelSpan, activeEventName, (e) => { - this._props.onActiveHandler(e); - }); - } - } + this._$labelSpan = $(''); + this._$label = $('
') + .addClass(LABEL_CLASS) + .append(this._$labelSpan); - _updateEditorLabelClass(visible) { - this._props.$editor - .removeClass(TEXTEDITOR_WITH_FLOATING_LABEL_CLASS) - .removeClass(TEXTEDITOR_LABEL_OUTSIDE_CLASS) - .removeClass(TEXTEDITOR_WITH_LABEL_CLASS); + this._$after = $('
').addClass(LABEL_AFTER_CLASS); - if(visible) { - const labelClass = this._props.mode === 'floating' - ? TEXTEDITOR_WITH_FLOATING_LABEL_CLASS - : TEXTEDITOR_WITH_LABEL_CLASS; + this._$root = $('
') + .addClass(TEXTEDITOR_LABEL_CLASS) + .attr('id', this._id) + .append(this._$before) + .append(this._$label) + .append(this._$after); - this._props.$editor.addClass(labelClass); + this._updateMark(); + this._updateText(); + this._updateBeforeWidth(); + this._updateMaxWidth(); + } - if(this._isOutsideMode()) { - this._props.$editor.addClass(TEXTEDITOR_LABEL_OUTSIDE_CLASS); - } - } - } + _toggleMarkupVisibility() { + const visible = this._isVisible(); - _isOutsideMode() { - return this._props.mode === 'outside'; - } + this._updateEditorBeforeButtonsClass(visible); + this._updateEditorLabelClass(visible); + + visible + ? this._$root.appendTo(this._props.$editor) + : this._$root.detach(); + + this._attachEvents(); + } + + _attachEvents() { + const clickEventName = addNamespace(click, this.NAME); + const hoverStartEventName = addNamespace(hoverStart, this.NAME); + const activeEventName = addNamespace(active, this.NAME); - _updateEditorBeforeButtonsClass(visible = this._isVisible()) { - this._props.$editor - .removeClass(TEXTEDITOR_WITH_BEFORE_BUTTONS_CLASS); + eventsEngine.off(this._$labelSpan, clickEventName); + eventsEngine.off(this._$labelSpan, hoverStartEventName); + eventsEngine.off(this._$labelSpan, activeEventName); - if(visible) { - const beforeButtonsClass = this._props.containsButtonsBefore ? TEXTEDITOR_WITH_BEFORE_BUTTONS_CLASS : ''; + if (this._isVisible() && this._isOutsideMode()) { + eventsEngine.on(this._$labelSpan, clickEventName, (e) => { + // @ts-expect-error + const selectedText = getWindow().getSelection().toString(); - this._props.$editor.addClass(beforeButtonsClass); + if (selectedText === '') { + this._props.onClickHandler(); + e.preventDefault(); } + }); + eventsEngine.on(this._$labelSpan, hoverStartEventName, (e) => { + this._props.onHoverHandler(e); + }); + eventsEngine.on(this._$labelSpan, activeEventName, (e) => { + this._props.onActiveHandler(e); + }); } + } - _updateMark() { - this._$labelSpan.attr('data-mark', this._props.mark); - } + _updateEditorLabelClass(visible) { + this._props.$editor + .removeClass(TEXTEDITOR_WITH_FLOATING_LABEL_CLASS) + .removeClass(TEXTEDITOR_LABEL_OUTSIDE_CLASS) + .removeClass(TEXTEDITOR_WITH_LABEL_CLASS); + + if (visible) { + const labelClass = this._props.mode === 'floating' + ? TEXTEDITOR_WITH_FLOATING_LABEL_CLASS + : TEXTEDITOR_WITH_LABEL_CLASS; + + this._props.$editor.addClass(labelClass); - _updateText() { - this._$labelSpan.text(this._props.text); + if (this._isOutsideMode()) { + this._props.$editor.addClass(TEXTEDITOR_LABEL_OUTSIDE_CLASS); + } } + } - _updateBeforeWidth() { - if(this._isVisible()) { - const width = this._props.beforeWidth ?? this._props.getBeforeWidth(); + _isOutsideMode() { + return this._props.mode === 'outside'; + } - this._$before.css({ width }); + _updateEditorBeforeButtonsClass(visible = this._isVisible()) { + this._props.$editor + .removeClass(TEXTEDITOR_WITH_BEFORE_BUTTONS_CLASS); - this._updateLabelTransform(); - } + if (visible) { + const beforeButtonsClass = this._props.containsButtonsBefore ? TEXTEDITOR_WITH_BEFORE_BUTTONS_CLASS : ''; + + this._props.$editor.addClass(beforeButtonsClass); } + } - _updateLabelTransform(offset = 0) { - this._$labelSpan.css('transform', ''); + _updateMark() { + this._$labelSpan.attr('data-mark', this._props.mark); + } - if(this._isVisible() && this._isOutsideMode()) { - const sign = this._props.rtlEnabled ? 1 : -1; + _updateText() { + this._$labelSpan.text(this._props.text); + } - const labelTranslateX = sign * (getWidth(this._$before) + offset); + _updateBeforeWidth() { + if (this._isVisible()) { + const width = this._props.beforeWidth ?? this._props.getBeforeWidth(); + // @ts-expect-error + this._$before.css({ width }); - this._$labelSpan.css('transform', `translateX(${labelTranslateX}px)`); - } + this._updateLabelTransform(); } + } - _updateMaxWidth() { - if(this._isVisible() && !this._isOutsideMode()) { - const maxWidth = this._props.containerWidth ?? this._props.getContainerWidth(); + _updateLabelTransform(offset = 0) { + this._$labelSpan.css('transform', ''); - this._$label.css({ maxWidth }); - } - } + if (this._isVisible() && this._isOutsideMode()) { + const sign = this._props.rtlEnabled ? 1 : -1; - $element() { - return this._$root; - } + const labelTranslateX = sign * (getWidth(this._$before) + offset); - isVisible() { - return this._isVisible(); + this._$labelSpan.css('transform', `translateX(${labelTranslateX}px)`); } + } - getId() { - if(this._isVisible()) return this._id; + _updateMaxWidth() { + if (this._isVisible() && !this._isOutsideMode()) { + const maxWidth = this._props.containerWidth ?? this._props.getContainerWidth(); + // @ts-expect-error + this._$label.css({ maxWidth }); } + } - updateMode(mode) { - this._props.mode = mode; - this._toggleMarkupVisibility(); - this._updateBeforeWidth(); - this._updateMaxWidth(); - } + $element() { + return this._$root; + } - updateText(text) { - this._props.text = text; - this._updateText(); - this._toggleMarkupVisibility(); - this._updateBeforeWidth(); - this._updateMaxWidth(); - } + isVisible() { + return this._isVisible(); + } - updateMark(mark) { - this._props.mark = mark; - this._updateMark(); - } + // @ts-expect-error + getId() { + if (this._isVisible()) return this._id; + } - updateContainsButtonsBefore(containsButtonsBefore) { - this._props.containsButtonsBefore = containsButtonsBefore; - this._updateEditorBeforeButtonsClass(); - } + updateMode(mode) { + this._props.mode = mode; + this._toggleMarkupVisibility(); + this._updateBeforeWidth(); + this._updateMaxWidth(); + } - updateBeforeWidth(beforeWidth) { - this._props.beforeWidth = beforeWidth; - this._updateBeforeWidth(); - } + updateText(text) { + this._props.text = text; + this._updateText(); + this._toggleMarkupVisibility(); + this._updateBeforeWidth(); + this._updateMaxWidth(); + } - updateMaxWidth(containerWidth) { - this._props.containerWidth = containerWidth; - this._updateMaxWidth(); - } + updateMark(mark) { + this._props.mark = mark; + this._updateMark(); + } + + updateContainsButtonsBefore(containsButtonsBefore) { + this._props.containsButtonsBefore = containsButtonsBefore; + this._updateEditorBeforeButtonsClass(); + } + + updateBeforeWidth(beforeWidth) { + this._props.beforeWidth = beforeWidth; + this._updateBeforeWidth(); + } + + updateMaxWidth(containerWidth) { + this._props.containerWidth = containerWidth; + this._updateMaxWidth(); + } } export { - TextEditorLabel + TextEditorLabel, }; diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.rule.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.rule.ts index 64aa230e460d..1cdb7981822b 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.rule.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.rule.ts @@ -1,283 +1,299 @@ -import { extend } from '../../core/utils/extend'; -import { isFunction } from '../../core/utils/type'; +/* eslint-disable max-classes-per-file */ +import { extend } from '@js/core/utils/extend'; +import { isFunction } from '@js/core/utils/type'; const EMPTY_CHAR = ' '; class BaseMaskRule { + _value: string; - constructor(config) { - this._value = EMPTY_CHAR; - extend(this, config); - } + maskChar?: string; - next(rule) { - if(!arguments.length) { - return this._next; - } + _next?: any; - this._next = rule; - } + constructor(config) { + this._value = EMPTY_CHAR; + extend(this, config); + } - _prepareHandlingArgs(args, config) { - config = config || {}; - const handlingProperty = Object.prototype.hasOwnProperty.call(args, 'value') ? 'value' : 'text'; - args[handlingProperty] = config.str ?? args[handlingProperty]; - args.start = config.start ?? args.start; - args.length = config.length ?? args.length; - args.index = args.index + 1; - return args; + next(rule?: any) { + if (!arguments.length) { + return this._next; } - first(index) { - index = index || 0; - return this.next().first(index + 1); - } + this._next = rule; + } - isAccepted() { - return false; - } + _prepareHandlingArgs(args, config?: any) { + config = config || {}; + const handlingProperty = Object.prototype.hasOwnProperty.call(args, 'value') ? 'value' : 'text'; + args[handlingProperty] = config.str ?? args[handlingProperty]; + args.start = config.start ?? args.start; + args.length = config.length ?? args.length; + args.index += 1; + return args; + } - adjustedCaret(caret, isForwardDirection, char) { - return isForwardDirection - ? this._adjustedForward(caret, 0, char) - : this._adjustedBackward(caret, 0, char); - } - _adjustedForward() {} - _adjustedBackward() {} + first(index) { + index = index || 0; + return this.next().first(index + 1); + } - isValid() {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isAccepted(caret?: any) { + return false; + } - reset() {} - clear() {} + adjustedCaret(caret, isForwardDirection, char) { + return isForwardDirection + ? this._adjustedForward(caret, 0, char) + : this._adjustedBackward(caret, 0, char); + } - text() {} - value() {} - rawValue() {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _adjustedForward(caret, index, char) {} - handle() {} -} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _adjustedBackward(caret, index, char?: string) {} -export class EmptyMaskRule extends BaseMaskRule { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isValid(args: any) {} - next() {} + reset() {} - handle() { - return 0; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + clear(args?: any) {} - text() { - return ''; - } + text() {} - value() { - return ''; - } + value() {} - first() { - return 0; - } + rawValue() {} - rawValue() { - return ''; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + handle(args) {} +} - adjustedCaret() { - return 0; - } +export class EmptyMaskRule extends BaseMaskRule { + next() {} - isValid() { - return true; - } + handle() { + return 0; + } -} + text() { + return ''; + } -export class MaskRule extends BaseMaskRule { + value() { + return ''; + } - text() { - return (this._value !== EMPTY_CHAR ? this._value : this.maskChar) + this.next().text(); - } + first() { + return 0; + } - value() { - return this._value + this.next().value(); - } + rawValue() { + return ''; + } - rawValue() { - return this._value + this.next().rawValue(); - } + adjustedCaret() { + return 0; + } - handle(args) { - const str = Object.prototype.hasOwnProperty.call(args, 'value') ? args.value : args.text; - if(!str || !str.length || !args.length) { - return 0; - } + isValid() { + return true; + } +} - if(args.start) { - return this.next().handle(this._prepareHandlingArgs(args, { start: args.start - 1 })); - } +export class MaskRule extends BaseMaskRule { + _isAccepted?: boolean; - const char = str[0]; - const rest = str.substring(1); + text() { + return (this._value !== EMPTY_CHAR ? this._value : this.maskChar) + this.next().text(); + } - this._tryAcceptChar(char, args); + value() { + return this._value + this.next().value(); + } - return this._accepted() - ? this.next().handle(this._prepareHandlingArgs(args, { str: rest, length: args.length - 1 })) + 1 - : this.handle(this._prepareHandlingArgs(args, { str: rest, length: args.length - 1 })); - } + rawValue() { + return this._value + this.next().rawValue(); + } - clear(args) { - this._tryAcceptChar(EMPTY_CHAR, args); - this.next().clear(this._prepareHandlingArgs(args)); + handle(args) { + const str = Object.prototype.hasOwnProperty.call(args, 'value') ? args.value : args.text; + if (!str || !str.length || !args.length) { + return 0; } - reset() { - this._accepted(false); - this.next().reset(); + if (args.start) { + return this.next().handle(this._prepareHandlingArgs(args, { start: args.start - 1 })); } - _tryAcceptChar(char, args) { - this._accepted(false); + const char = str[0]; + const rest = str.substring(1); - if(!this._isAllowed(char, args)) { - return; - } - const acceptedChar = char === EMPTY_CHAR ? this.maskChar : char; - args.fullText = args.fullText.substring(0, args.index) + acceptedChar + args.fullText.substring(args.index + 1); - this._accepted(true); - this._value = char; - } + this._tryAcceptChar(char, args); + + return this._accepted() + ? this.next().handle(this._prepareHandlingArgs(args, { str: rest, length: args.length - 1 })) + 1 + : this.handle(this._prepareHandlingArgs(args, { str: rest, length: args.length - 1 })); + } + + clear(args) { + this._tryAcceptChar(EMPTY_CHAR, args); + this.next().clear(this._prepareHandlingArgs(args)); + } - _accepted(value) { - if(!arguments.length) { - return !!this._isAccepted; - } - this._isAccepted = !!value; + reset() { + this._accepted(false); + this.next().reset(); + } + + _tryAcceptChar(char, args) { + this._accepted(false); + + if (!this._isAllowed(char, args)) { + return; } + const acceptedChar = char === EMPTY_CHAR ? this.maskChar : char; + args.fullText = args.fullText.substring(0, args.index) + acceptedChar + args.fullText.substring(args.index + 1); + this._accepted(true); + this._value = char; + } - first(index) { - return this._value === EMPTY_CHAR - ? index || 0 - : super.first(index); + // @ts-expect-error + _accepted(value?: any) { + if (!arguments.length) { + return !!this._isAccepted; } + this._isAccepted = !!value; + } - _isAllowed(char, args) { - if(char === EMPTY_CHAR) { - return true; - } + first(index) { + return this._value === EMPTY_CHAR + ? index || 0 + : super.first(index); + } - return this._isValid(char, args); + _isAllowed(char, args?: any) { + if (char === EMPTY_CHAR) { + return true; } - _isValid(char, args) { - const allowedChars = this.allowedChars; - - if(allowedChars instanceof RegExp) { - return allowedChars.test(char); - } + return this._isValid(char, args); + } - if(isFunction(allowedChars)) { - return allowedChars(char, args.index, args.fullText); - } + _isValid(char, args) { + // @ts-expect-error + const { allowedChars } = this; - if(Array.isArray(allowedChars)) { - return allowedChars.includes(char); - } + if (allowedChars instanceof RegExp) { + return allowedChars.test(char); + } - return allowedChars === char; + if (isFunction(allowedChars)) { + return allowedChars(char, args.index, args.fullText); } - isAccepted(caret) { - return (caret === 0) - ? this._accepted() - : this.next().isAccepted(caret - 1); + if (Array.isArray(allowedChars)) { + return allowedChars.includes(char); } - _adjustedForward(caret, index, char) { - if(index >= caret) { - return index; - } + return allowedChars === char; + } + + isAccepted(caret) { + return caret === 0 + ? this._accepted() + : this.next().isAccepted(caret - 1); + } - return this.next()._adjustedForward(caret, index + 1, char) || index + 1; + _adjustedForward(caret, index, char) { + if (index >= caret) { + return index; } - _adjustedBackward(caret, index) { - if(index >= caret - 1) { - return caret; - } + return this.next()._adjustedForward(caret, index + 1, char) || index + 1; + } - return this.next()._adjustedBackward(caret, index + 1) || index + 1; + _adjustedBackward(caret, index) { + if (index >= caret - 1) { + return caret; } - isValid(args) { - return this._isValid(this._value, args) && this.next().isValid(this._prepareHandlingArgs(args)); - } + return this.next()._adjustedBackward(caret, index + 1) || index + 1; + } + isValid(args) { + return this._isValid(this._value, args) && this.next().isValid(this._prepareHandlingArgs(args)); + } } export class StubMaskRule extends MaskRule { + value() { + return this.next().value(); + } - value() { - return this.next().value(); + handle(args) { + const hasValueProperty = Object.prototype.hasOwnProperty.call(args, 'value'); + const str = hasValueProperty ? args.value : args.text; + if (!str.length || !args.length) { + return 0; } - handle(args) { - const hasValueProperty = Object.prototype.hasOwnProperty.call(args, 'value'); - const str = hasValueProperty ? args.value : args.text; - if(!str.length || !args.length) { - return 0; - } + if (args.start || hasValueProperty) { + return this.next().handle(this._prepareHandlingArgs(args, { start: args.start && args.start - 1 })); + } - if(args.start || hasValueProperty) { - return this.next().handle(this._prepareHandlingArgs(args, { start: args.start && args.start - 1 })); - } + const char = str[0]; + const rest = str.substring(1); - const char = str[0]; - const rest = str.substring(1); + this._tryAcceptChar(char); - this._tryAcceptChar(char); + const nextArgs = this._isAllowed(char) ? this._prepareHandlingArgs(args, { str: rest, length: args.length - 1 }) : args; + return this.next().handle(nextArgs) + 1; + } - const nextArgs = this._isAllowed(char) ? this._prepareHandlingArgs(args, { str: rest, length: args.length - 1 }) : args; - return this.next().handle(nextArgs) + 1; - } + clear(args) { + this._accepted(false); + this.next().clear(this._prepareHandlingArgs(args)); + } - clear(args) { - this._accepted(false); - this.next().clear(this._prepareHandlingArgs(args)); - } + _tryAcceptChar(char) { + this._accepted(this._isValid(char)); + } - _tryAcceptChar(char) { - this._accepted(this._isValid(char)); - } + _isValid(char) { + return char === this.maskChar; + } - _isValid(char) { - return char === this.maskChar; - } + first(index) { + index = index || 0; + return this.next().first(index + 1); + } - first(index) { - index = index || 0; - return this.next().first(index + 1); + _adjustedForward(caret, index, char) { + if (index >= caret && char === this.maskChar) { + return index; } - _adjustedForward(caret, index, char) { - if(index >= caret && char === this.maskChar) { - return index; - } - - if(caret === (index + 1) && this._accepted()) { - return caret; - } - return this.next()._adjustedForward(caret, index + 1, char); + if (caret === (index + 1) && this._accepted()) { + return caret; } + return this.next()._adjustedForward(caret, index + 1, char); + } - _adjustedBackward(caret, index) { - if(index >= caret - 1) { - return 0; - } - - return this.next()._adjustedBackward(caret, index + 1); + _adjustedBackward(caret, index) { + if (index >= caret - 1) { + return 0; } - isValid(args) { - return this.next().isValid(this._prepareHandlingArgs(args)); - } + return this.next()._adjustedBackward(caret, index + 1); + } + + isValid(args) { + return this.next().isValid(this._prepareHandlingArgs(args)); + } } diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.strategy.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.strategy.ts index e322b9e47f56..772fc3a57beb 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.strategy.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.strategy.ts @@ -1,7 +1,7 @@ -import EventsEngine from '../../events/core/events_engine'; -import { addNamespace } from '../../events/utils/index'; -import browser from '../../core/utils/browser'; -import { clipboardText as getClipboardText } from '../../core/utils/dom'; +import browser from '@js/core/utils/browser'; +import { clipboardText as getClipboardText } from '@js/core/utils/dom'; +import EventsEngine from '@js/events/core/events_engine'; +import { addNamespace } from '@js/events/utils/index'; const MASK_EVENT_NAMESPACE = 'dxMask'; const BLUR_EVENT = 'blur beforedeactivate'; @@ -11,280 +11,297 @@ const HISTORY_INPUT_TYPES = ['historyUndo', 'historyRedo']; const EVENT_NAMES = ['focusIn', 'focusOut', 'input', 'paste', 'cut', 'drop', 'beforeInput']; function getEmptyString(length) { - return EMPTY_CHAR.repeat(length); + return EMPTY_CHAR.repeat(length); } export default class MaskStrategy { - constructor(editor) { - this.editor = editor; - } + editor: any; - _editorOption() { - return this.editor.option(...arguments); - } + _dragTimer?: any; - _editorInput() { - return this.editor._input(); - } + _inputHandlerTimer?: any; - _editorCaret(newCaret) { - if(!newCaret) { - return this.editor._caret(); - } - - this.editor._caret(newCaret); - } + _caretTimeout?: any; - _attachChangeEventHandler() { - if(!this._editorOption('valueChangeEvent').split(' ').includes('change')) { - return; - } + _prevCaret?: any; - const $input = this._editorInput(); - const namespace = addNamespace(BLUR_EVENT, MASK_EVENT_NAMESPACE); - EventsEngine.on($input, namespace, (e) => { - this.editor._changeHandler(e); - }); - } + _previousText?: string; - _beforeInputHandler() { - this._previousText = this._editorOption('text'); - this._prevCaret = this._editorCaret(); - } + constructor(editor) { + this.editor = editor; + } - _inputHandler(event) { - const { originalEvent } = event; - if(!originalEvent) { - return; - } - - const { inputType } = originalEvent; - - if(HISTORY_INPUT_TYPES.includes(inputType)) { - this._handleHistoryInputEvent(); - } else if(DELETE_INPUT_TYPES.includes(inputType)) { - this._handleBackwardDeleteInputEvent(); - } else { - const currentCaret = this._editorCaret(); - - if(!currentCaret.end) { - return; - } - - this._clearSelectedText(); - this._autoFillHandler(originalEvent); - this._editorCaret(currentCaret); - this._handleInsertTextInputEvent(originalEvent.data); - } - - if(this._editorOption('text') === this._previousText) { - event.stopImmediatePropagation(); - } - } + _editorOption() { + return this.editor.option(...arguments); + } - _handleHistoryInputEvent() { - const caret = this._editorCaret(); + _editorInput() { + return this.editor._input(); + } - this._updateEditorMask({ - start: caret.start, - length: caret.end - caret.start, - text: '' - }); - - this._editorCaret(this._prevCaret); + _editorCaret(newCaret?: any) { + if (!newCaret) { + return this.editor._caret(); } - _handleBackwardDeleteInputEvent() { - this._clearSelectedText(); - - const caret = this._editorCaret(); - this.editor.setForwardDirection(); - this.editor._adjustCaret(); + this.editor._caret(newCaret); + } - const adjustedForwardCaret = this._editorCaret(); - if(adjustedForwardCaret.start !== caret.start) { - this.editor.setBackwardDirection(); - this.editor._adjustCaret(); - } + _attachChangeEventHandler() { + // @ts-expect-error + if (!this._editorOption('valueChangeEvent').split(' ').includes('change')) { + return; } - _clearSelectedText() { - const length = (this._prevCaret?.end - this._prevCaret?.start) || 1; - const caret = this._editorCaret(); - - if(!this._isAutoFill()) { - this.editor.setBackwardDirection(); - this._updateEditorMask({ - start: caret.start, - length, - text: getEmptyString(length) - }); - } + const $input = this._editorInput(); + const namespace = addNamespace(BLUR_EVENT, MASK_EVENT_NAMESPACE); + EventsEngine.on($input, namespace, (e) => { + this.editor._changeHandler(e); + }); + } + + _beforeInputHandler() { + // @ts-expect-error + this._previousText = this._editorOption('text'); + this._prevCaret = this._editorCaret(); + } + + _inputHandler(event) { + const { originalEvent } = event; + if (!originalEvent) { + return; } - _handleInsertTextInputEvent(data) { - // NOTE: data has length > 1 when autosuggestion is applied. - const text = data ?? ''; + const { inputType } = originalEvent; - this.editor.setForwardDirection(); + if (HISTORY_INPUT_TYPES.includes(inputType)) { + this._handleHistoryInputEvent(); + } else if (DELETE_INPUT_TYPES.includes(inputType)) { + this._handleBackwardDeleteInputEvent(); + } else { + const currentCaret = this._editorCaret(); - const hasValidChars = this._updateEditorMask({ - start: this._prevCaret?.start ?? 0, - length: text.length || 1, - text - }); + if (!currentCaret.end) { + return; + } - if(!hasValidChars) { - this._editorCaret(this._prevCaret); - } + this._clearSelectedText(); + this._autoFillHandler(originalEvent); + this._editorCaret(currentCaret); + this._handleInsertTextInputEvent(originalEvent.data); } + // @ts-expect-error + if (this._editorOption('text') === this._previousText) { + event.stopImmediatePropagation(); + } + } - _updateEditorMask(args) { - const textLength = args.text.length; - const processedCharsCount = this.editor._handleChain(args); + _handleHistoryInputEvent() { + const caret = this._editorCaret(); - this.editor._displayMask(); + this._updateEditorMask({ + start: caret.start, + length: caret.end - caret.start, + text: '', + }); - if(this.editor.isForwardDirection()) { - const { start, end } = this._editorCaret(); - const correction = processedCharsCount - textLength; + this._editorCaret(this._prevCaret); + } - const hasSkippedStub = processedCharsCount > 1; - if(hasSkippedStub && textLength === 1) { - this._editorCaret({ start: start + correction, end: end + correction }); - } + _handleBackwardDeleteInputEvent() { + this._clearSelectedText(); - this.editor._adjustCaret(); - } + const caret = this._editorCaret(); + this.editor.setForwardDirection(); + this.editor._adjustCaret(); - return !!processedCharsCount; + const adjustedForwardCaret = this._editorCaret(); + if (adjustedForwardCaret.start !== caret.start) { + this.editor.setBackwardDirection(); + this.editor._adjustCaret(); } - - _focusInHandler() { - this.editor._showMaskPlaceholder(); - this.editor.setForwardDirection(); - - if(!this.editor._isValueEmpty() && this._editorOption('isValid')) { - this.editor._adjustCaret(); - } else { - const caret = this.editor._maskRulesChain.first(); - this._caretTimeout = setTimeout(() => { - this._editorCaret({ start: caret, end: caret }); - }, 0); - } + } + + _clearSelectedText() { + // eslint-disable-next-line no-unsafe-optional-chaining + const length = (this._prevCaret?.end - this._prevCaret?.start) || 1; + const caret = this._editorCaret(); + + if (!this._isAutoFill()) { + this.editor.setBackwardDirection(); + this._updateEditorMask({ + start: caret.start, + length, + text: getEmptyString(length), + }); } + } - _focusOutHandler(event) { - this.editor._changeHandler(event); + _handleInsertTextInputEvent(data) { + // NOTE: data has length > 1 when autosuggestion is applied. + const text = data ?? ''; - if(this._editorOption('showMaskMode') === 'onFocus' && this.editor._isValueEmpty()) { - this._editorOption('text', ''); - this.editor._renderDisplayText(''); - } - } + this.editor.setForwardDirection(); - _delHandler(event) { - const { editor } = this; + const hasValidChars = this._updateEditorMask({ + start: this._prevCaret?.start ?? 0, + length: text.length || 1, + text, + }); - editor._maskKeyHandler(event, () => { - if(!editor._hasSelection()) { - editor._handleKey(EMPTY_CHAR); - } - }); + if (!hasValidChars) { + this._editorCaret(this._prevCaret); } + } + + _updateEditorMask(args) { + const textLength = args.text.length; + const processedCharsCount = this.editor._handleChain(args); - _cutHandler(event) { - const caret = this._editorCaret(); - const selectedText = this._editorInput().val().substring(caret.start, caret.end); + this.editor._displayMask(); - this.editor._maskKeyHandler(event, () => getClipboardText(event, selectedText)); + if (this.editor.isForwardDirection()) { + const { start, end } = this._editorCaret(); + const correction = processedCharsCount - textLength; + + const hasSkippedStub = processedCharsCount > 1; + if (hasSkippedStub && textLength === 1) { + this._editorCaret({ start: start + correction, end: end + correction }); + } + + this.editor._adjustCaret(); } - _dropHandler() { - this._clearDragTimer(); - this._dragTimer = setTimeout(() => { - const value = this.editor._convertToValue(this._editorInput().val()); - this._editorOption('value', value); - }); + return !!processedCharsCount; + } + + _focusInHandler() { + this.editor._showMaskPlaceholder(); + this.editor.setForwardDirection(); + // @ts-expect-error + if (!this.editor._isValueEmpty() && this._editorOption('isValid')) { + this.editor._adjustCaret(); + } else { + const caret = this.editor._maskRulesChain.first(); + this._caretTimeout = setTimeout(() => { + this._editorCaret({ start: caret, end: caret }); + }, 0); + } + } + + _focusOutHandler(event) { + this.editor._changeHandler(event); + // @ts-expect-error + if (this._editorOption('showMaskMode') === 'onFocus' && this.editor._isValueEmpty()) { + // @ts-expect-error + this._editorOption('text', ''); + this.editor._renderDisplayText(''); + } + } + + _delHandler(event) { + const { editor } = this; + + editor._maskKeyHandler(event, () => { + if (!editor._hasSelection()) { + editor._handleKey(EMPTY_CHAR); + } + }); + } + + _cutHandler(event) { + const caret = this._editorCaret(); + const selectedText = this._editorInput().val().substring(caret.start, caret.end); + + this.editor._maskKeyHandler(event, () => getClipboardText(event, selectedText)); + } + + _dropHandler() { + this._clearDragTimer(); + this._dragTimer = setTimeout(() => { + const value = this.editor._convertToValue(this._editorInput().val()); + // @ts-expect-error + this._editorOption('value', value); + }); + } + + _pasteHandler(event) { + const { editor } = this; + // @ts-expect-error + if (this._editorOption('disabled')) { + return; } - _pasteHandler(event) { - const { editor } = this; + const caret = this._editorCaret(); - if(this._editorOption('disabled')) { - return; - } + editor._maskKeyHandler(event, () => { + const pastedText = getClipboardText(event); + const restText = editor._maskRulesChain.text().substring(caret.end); + const accepted = editor._handleChain({ text: pastedText, start: caret.start, length: pastedText.length }); + const newCaret = caret.start + accepted; - const caret = this._editorCaret(); + editor._handleChain({ text: restText, start: newCaret, length: restText.length }); + editor._caret({ start: newCaret, end: newCaret }); + }); + } + _autoFillHandler(event) { + const { editor } = this; + const inputVal = this._editorInput().val(); + this._inputHandlerTimer = setTimeout(() => { + if (this._isAutoFill()) { editor._maskKeyHandler(event, () => { - const pastedText = getClipboardText(event); - const restText = editor._maskRulesChain.text().substring(caret.end); - const accepted = editor._handleChain({ text: pastedText, start: caret.start, length: pastedText.length }); - const newCaret = caret.start + accepted; - - editor._handleChain({ text: restText, start: newCaret, length: restText.length }); - editor._caret({ start: newCaret, end: newCaret }); + editor._handleChain({ text: inputVal, start: 0, length: inputVal.length }); }); - } - - _autoFillHandler(event) { - const { editor } = this; - const inputVal = this._editorInput().val(); - this._inputHandlerTimer = setTimeout(() => { - if(this._isAutoFill()) { - editor._maskKeyHandler(event, () => { - editor._handleChain({ text: inputVal, start: 0, length: inputVal.length }); - }); - editor._validateMask(); - } - }); - } + editor._validateMask(); + } + }); + } - _isAutoFill() { - const $input = this._editorInput(); + _isAutoFill() { + const $input = this._editorInput(); - if(browser.webkit) { - const input = $input.get(0); - return input?.matches(':-webkit-autofill') ?? false; - } - - return false; + if (browser.webkit) { + const input = $input.get(0); + return input?.matches(':-webkit-autofill') ?? false; } - _clearDragTimer() { - clearTimeout(this._dragTimer); - } + return false; + } - _clearTimers() { - this._clearDragTimer(); - clearTimeout(this._caretTimeout); - clearTimeout(this._inputHandlerTimer); - } + _clearDragTimer() { + clearTimeout(this._dragTimer); + } - getHandler(handlerName) { - return (args) => { - this[`_${handlerName}Handler`]?.(args); - }; - } + _clearTimers() { + this._clearDragTimer(); + clearTimeout(this._caretTimeout); + clearTimeout(this._inputHandlerTimer); + } - attachEvents() { - const $input = this._editorInput(); + getHandler(handlerName) { + return (args) => { + this[`_${handlerName}Handler`]?.(args); + }; + } - EVENT_NAMES.forEach((eventName) => { - const namespace = addNamespace(eventName.toLowerCase(), MASK_EVENT_NAMESPACE); - EventsEngine.on($input, namespace, this.getHandler(eventName)); - }); + attachEvents() { + const $input = this._editorInput(); - this._attachChangeEventHandler(); - } + EVENT_NAMES.forEach((eventName) => { + const namespace = addNamespace(eventName.toLowerCase(), MASK_EVENT_NAMESPACE); + EventsEngine.on($input, namespace, this.getHandler(eventName)); + }); - detachEvents() { - this._clearTimers(); - EventsEngine.off(this._editorInput(), `.${MASK_EVENT_NAMESPACE}`); - } + this._attachChangeEventHandler(); + } - clean() { - this._clearTimers(); - } + detachEvents() { + this._clearTimers(); + EventsEngine.off(this._editorInput(), `.${MASK_EVENT_NAMESPACE}`); + } + + clean() { + this._clearTimers(); + } } diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.ts index 3b27e5ac9bdd..3441f1c700b7 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.mask.ts @@ -1,18 +1,21 @@ -import $ from '../../core/renderer'; -import caretUtils from './utils.caret'; -import { each } from '../../core/utils/iterator'; -import { addNamespace, createEvent, isCommandKeyPressed, normalizeKeyName } from '../../events/utils/index'; -import eventsEngine from '../../events/core/events_engine'; -import { extend } from '../../core/utils/extend'; -import { focused } from '../widget/selectors'; -import { isDefined } from '../../core/utils/type'; -import messageLocalization from '../../localization/message'; -import { noop } from '../../core/utils/common'; -import { isEmpty } from '../../core/utils/string'; -import { name as wheelEventName } from '../../events/core/wheel'; -import { EmptyMaskRule, StubMaskRule, MaskRule } from './ui.text_editor.mask.rule'; -import TextEditorBase from './ui.text_editor.base'; -import MaskStrategy from './ui.text_editor.mask.strategy'; +import $ from '@js/core/renderer'; +import { noop } from '@js/core/utils/common'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { isEmpty } from '@js/core/utils/string'; +import { isDefined } from '@js/core/utils/type'; +import eventsEngine from '@js/events/core/events_engine'; +import { name as wheelEventName } from '@js/events/core/wheel'; +import { + addNamespace, createEvent, isCommandKeyPressed, normalizeKeyName, +} from '@js/events/utils/index'; +import messageLocalization from '@js/localization/message'; +import { focused } from '@js/ui/widget/selectors'; + +import TextEditorBase from './m_text_editor.base'; +import { EmptyMaskRule, MaskRule, StubMaskRule } from './m_text_editor.mask.rule'; +import MaskStrategy from './m_text_editor.mask.strategy'; +import caretUtils from './m_utils.caret'; const caret = caretUtils; @@ -26,566 +29,569 @@ const BACKWARD_DIRECTION = 'backward'; const DROP_EVENT_NAME = 'drop'; const buildInMaskRules = { - '0': /[0-9]/, - '9': /[0-9\s]/, - '#': /[-+0-9\s]/, - 'L': function(char) { - return isLiteralChar(char); - }, - 'l': function(char) { - return isLiteralChar(char) || isSpaceChar(char); - }, - 'C': /\S/, - 'c': /./, - 'A': function(char) { - return isLiteralChar(char) || isNumericChar(char); - }, - 'a': function(char) { - return isLiteralChar(char) || isNumericChar(char) || isSpaceChar(char); - } + 0: /[0-9]/, + 9: /[0-9\s]/, + '#': /[-+0-9\s]/, + L(char) { + return isLiteralChar(char); + }, + l(char) { + return isLiteralChar(char) || isSpaceChar(char); + }, + C: /\S/, + c: /./, + A(char) { + return isLiteralChar(char) || isNumericChar(char); + }, + a(char) { + return isLiteralChar(char) || isNumericChar(char) || isSpaceChar(char); + }, }; function isNumericChar(char) { - return /[0-9]/.test(char); + return /[0-9]/.test(char); } function isLiteralChar(char) { - const code = char.charCodeAt(); - return (64 < code && code < 91 || 96 < code && code < 123 || code > 127); + const code = char.charCodeAt(); + return code > 64 && code < 91 || code > 96 && code < 123 || code > 127; } function isSpaceChar(char) { - return char === ' '; + return char === ' '; } const TextEditorMask = TextEditorBase.inherit({ - _getDefaultOptions: function() { - return extend(this.callBase(), { - mask: '', + _getDefaultOptions() { + return extend(this.callBase(), { + mask: '', - maskChar: '_', + maskChar: '_', - maskRules: {}, + maskRules: {}, - maskInvalidMessage: messageLocalization.format('validation-mask'), + maskInvalidMessage: messageLocalization.format('validation-mask'), - useMaskedValue: false, + useMaskedValue: false, - showMaskMode: 'always' - }); - }, + showMaskMode: 'always', + }); + }, - _supportedKeys: function() { - const that = this; + _supportedKeys() { + const that = this; - const keyHandlerMap = { - del: that._maskStrategy.getHandler('del'), - enter: that._changeHandler - }; + const keyHandlerMap = { + del: that._maskStrategy.getHandler('del'), + enter: that._changeHandler, + }; - const result = that.callBase(); - each(keyHandlerMap, function(key, callback) { - const parentHandler = result[key]; - result[key] = function(e) { - that.option('mask') && callback.call(that, e); - parentHandler && parentHandler(e); - }; - }); - - return result; - }, - - _getSubmitElement: function() { - return !this.option('mask') ? this.callBase() : this._$hiddenElement; - }, - - _init: function() { - this.callBase(); - - this._initMaskStrategy(); - }, - - _initMaskStrategy: function() { - this._maskStrategy = new MaskStrategy(this); - }, - - _initMarkup: function() { - this._renderHiddenElement(); - this.callBase(); - }, - - _attachMouseWheelEventHandlers: function() { - const hasMouseWheelHandler = this._onMouseWheel !== noop; - - if(!hasMouseWheelHandler) { - return; - } - - const input = this._input(); - const eventName = addNamespace(wheelEventName, this.NAME); - const mouseWheelAction = this._createAction((function(e) { - const { event } = e; - - if(focused(input) && !isCommandKeyPressed(event)) { - this._onMouseWheel(event); - event.preventDefault(); - event.stopPropagation(); - } - }).bind(this)); - - eventsEngine.off(input, eventName); - eventsEngine.on(input, eventName, function(e) { - mouseWheelAction({ event: e }); - }); - }, - - _onMouseWheel: noop, - - _useMaskBehavior() { - return Boolean(this.option('mask')); - }, - - _attachDropEventHandler() { - const useMaskBehavior = this._useMaskBehavior(); - - if(!useMaskBehavior) { - return; - } - - const eventName = addNamespace(DROP_EVENT_NAME, this.NAME); - const input = this._input(); - - eventsEngine.off(input, eventName); - eventsEngine.on(input, eventName, (e) => e.preventDefault()); - }, - - _render() { - this._attachMouseWheelEventHandlers(); - this._renderMask(); - this.callBase(); - this._attachDropEventHandler(); - }, - - _renderHiddenElement: function() { - if(this.option('mask')) { - this._$hiddenElement = $('') - .attr('type', 'hidden') - .appendTo(this._inputWrapper()); - } - }, - - _removeHiddenElement: function() { - this._$hiddenElement && this._$hiddenElement.remove(); - }, - - _renderMask: function() { - this.$element().removeClass(TEXTEDITOR_MASKED_CLASS); - this._maskRulesChain = null; - - this._maskStrategy.detachEvents(); - - if(!this.option('mask')) { - return; - } - - this.$element().addClass(TEXTEDITOR_MASKED_CLASS); + const result = that.callBase(); + each(keyHandlerMap, (key, callback) => { + const parentHandler = result[key]; + result[key] = function (e) { + that.option('mask') && callback.call(that, e); + parentHandler && parentHandler(e); + }; + }); - this._maskStrategy.attachEvents(); - this._parseMask(); - this._renderMaskedValue(); - }, - - _changeHandler: function(e) { - const $input = this._input(); - const inputValue = $input.val(); - - if(inputValue === this._changedValue) { - return; - } - - this._changedValue = inputValue; - const changeEvent = createEvent(e, { type: 'change' }); - eventsEngine.trigger($input, changeEvent); - }, - - _parseMask: function() { - this._maskRules = extend({}, buildInMaskRules, this.option('maskRules')); - this._maskRulesChain = this._parseMaskRule(0); - }, - - _parseMaskRule: function(index) { - const mask = this.option('mask'); - if(index >= mask.length) { - return new EmptyMaskRule(); - } - - const currentMaskChar = mask[index]; - const isEscapedChar = currentMaskChar === ESCAPED_CHAR; - const result = isEscapedChar - ? new StubMaskRule({ maskChar: mask[index + 1] }) - : this._getMaskRule(currentMaskChar); - - result.next(this._parseMaskRule(index + 1 + isEscapedChar)); - return result; - }, - - _getMaskRule: function(pattern) { - let ruleConfig; - - each(this._maskRules, function(rulePattern, allowedChars) { - if(rulePattern === pattern) { - ruleConfig = { - pattern: rulePattern, - allowedChars: allowedChars - }; - return false; - } - }); - - return isDefined(ruleConfig) - ? new MaskRule(extend({ maskChar: this.option('maskChar') || ' ' }, ruleConfig)) - : new StubMaskRule({ maskChar: pattern }); - }, - - _renderMaskedValue: function() { - if(!this._maskRulesChain) { - return; - } - - const value = this.option('value') || ''; - this._maskRulesChain.clear(this._normalizeChainArguments()); - - const chainArgs = { length: value.length }; - chainArgs[this._isMaskedValueMode() ? 'text' : 'value'] = value; - - this._handleChain(chainArgs); - this._displayMask(); - }, - - _replaceSelectedText: function(text, selection, char) { - if(char === undefined) { - return text; - } - - const textBefore = text.slice(0, selection.start); - const textAfter = text.slice(selection.end); - const edited = textBefore + char + textAfter; - - return edited; - }, - - _isMaskedValueMode: function() { - return this.option('useMaskedValue'); - }, - - _displayMask: function(caret) { - caret = caret || this._caret(); - this._renderValue(); - this._caret(caret); - }, - - _isValueEmpty: function() { - return isEmpty(this._value); - }, - - _shouldShowMask: function() { - const showMaskMode = this.option('showMaskMode'); - - if(showMaskMode === 'onFocus') { - return focused(this._input()) || !this._isValueEmpty(); - } - - return true; - }, - - _showMaskPlaceholder: function() { - if(this._shouldShowMask()) { - const text = this._maskRulesChain.text(); - this.option('text', text); - if(this.option('showMaskMode') === 'onFocus') { - this._renderDisplayText(text); - } - } - }, - - _renderValue: function() { - if(this._maskRulesChain) { - this._showMaskPlaceholder(); - - if(this._$hiddenElement) { - const value = this._maskRulesChain.value(); - const submitElementValue = !isEmpty(value) ? - this._getPreparedValue() : - ''; - - this._$hiddenElement.val(submitElementValue); - } - } - return this.callBase(); - }, - - _getPreparedValue: function() { - return this._convertToValue().replace(/\s+$/, ''); - }, - - _valueChangeEventHandler: function(e) { - if(!this._maskRulesChain) { - this.callBase.apply(this, arguments); - return; - } - - this._saveValueChangeEvent(e); - - this.option('value', this._getPreparedValue()); - }, - - _isControlKeyFired: function(e) { - return this._isControlKey(normalizeKeyName(e)) || isCommandKeyPressed(e); - }, - - _handleChain: function(args) { - const handledCount = this._maskRulesChain.handle(this._normalizeChainArguments(args)); - this._updateMaskInfo(); - return handledCount; - }, - - _normalizeChainArguments: function(args) { - args = args || {}; - args.index = 0; - args.fullText = this._maskRulesChain.text(); - return args; - }, - - _convertToValue: function(text) { - if(this._isMaskedValueMode()) { - text = this._replaceMaskCharWithEmpty(text || this._textValue || ''); - } else { - text = text || this._value || ''; - } - - return text; - }, - - _replaceMaskCharWithEmpty: function(text) { - return text.replace(new RegExp(this.option('maskChar'), 'g'), EMPTY_CHAR); - }, - - _maskKeyHandler: function(e, keyHandler) { - if(this.option('readOnly')) { - return; - } - - this.setForwardDirection(); - e.preventDefault(); - - this._handleSelection(); - - const previousText = this._input().val(); - const raiseInputEvent = () => { - if(previousText !== this._input().val()) { - eventsEngine.trigger(this._input(), 'input'); - } + return result; + }, + + _getSubmitElement() { + return !this.option('mask') ? this.callBase() : this._$hiddenElement; + }, + + _init() { + this.callBase(); + + this._initMaskStrategy(); + }, + + _initMaskStrategy() { + this._maskStrategy = new MaskStrategy(this); + }, + + _initMarkup() { + this._renderHiddenElement(); + this.callBase(); + }, + + _attachMouseWheelEventHandlers() { + const hasMouseWheelHandler = this._onMouseWheel !== noop; + + if (!hasMouseWheelHandler) { + return; + } + + const input = this._input(); + const eventName = addNamespace(wheelEventName, this.NAME); + const mouseWheelAction = this._createAction((e) => { + const { event } = e; + + if (focused(input) && !isCommandKeyPressed(event)) { + this._onMouseWheel(event); + event.preventDefault(); + event.stopPropagation(); + } + }); + + eventsEngine.off(input, eventName); + eventsEngine.on(input, eventName, (e) => { + mouseWheelAction({ event: e }); + }); + }, + + _onMouseWheel: noop, + + _useMaskBehavior() { + return Boolean(this.option('mask')); + }, + + _attachDropEventHandler() { + const useMaskBehavior = this._useMaskBehavior(); + + if (!useMaskBehavior) { + return; + } + + const eventName = addNamespace(DROP_EVENT_NAME, this.NAME); + const input = this._input(); + + eventsEngine.off(input, eventName); + eventsEngine.on(input, eventName, (e) => e.preventDefault()); + }, + + _render() { + this._attachMouseWheelEventHandlers(); + this._renderMask(); + this.callBase(); + this._attachDropEventHandler(); + }, + + _renderHiddenElement() { + if (this.option('mask')) { + this._$hiddenElement = $('') + .attr('type', 'hidden') + .appendTo(this._inputWrapper()); + } + }, + + _removeHiddenElement() { + this._$hiddenElement && this._$hiddenElement.remove(); + }, + + _renderMask() { + this.$element().removeClass(TEXTEDITOR_MASKED_CLASS); + this._maskRulesChain = null; + + this._maskStrategy.detachEvents(); + + if (!this.option('mask')) { + return; + } + + this.$element().addClass(TEXTEDITOR_MASKED_CLASS); + + this._maskStrategy.attachEvents(); + this._parseMask(); + this._renderMaskedValue(); + }, + + _changeHandler(e) { + const $input = this._input(); + const inputValue = $input.val(); + + if (inputValue === this._changedValue) { + return; + } + + this._changedValue = inputValue; + const changeEvent = createEvent(e, { type: 'change' }); + // @ts-expect-error + eventsEngine.trigger($input, changeEvent); + }, + + _parseMask() { + this._maskRules = extend({}, buildInMaskRules, this.option('maskRules')); + this._maskRulesChain = this._parseMaskRule(0); + }, + + _parseMaskRule(index) { + const mask = this.option('mask'); + if (index >= mask.length) { + // @ts-expect-error + return new EmptyMaskRule(); + } + + const currentMaskChar = mask[index]; + const isEscapedChar = currentMaskChar === ESCAPED_CHAR; + const result = isEscapedChar + ? new StubMaskRule({ maskChar: mask[index + 1] }) + : this._getMaskRule(currentMaskChar); + + result.next(this._parseMaskRule(index + 1 + isEscapedChar)); + return result; + }, + + _getMaskRule(pattern) { + let ruleConfig; + // @ts-expect-error + each(this._maskRules, (rulePattern, allowedChars) => { + if (rulePattern === pattern) { + ruleConfig = { + pattern: rulePattern, + allowedChars, }; + return false; + } + }); + + return isDefined(ruleConfig) + ? new MaskRule(extend({ maskChar: this.option('maskChar') || ' ' }, ruleConfig)) + : new StubMaskRule({ maskChar: pattern }); + }, + + _renderMaskedValue() { + if (!this._maskRulesChain) { + return; + } - const handled = keyHandler(); - - if(handled) { - handled.then(raiseInputEvent); - } else { - this.setForwardDirection(); - this._adjustCaret(); - this._displayMask(); - this._maskRulesChain.reset(); - raiseInputEvent(); - } - }, - - _handleKey: function(key, direction) { - this._direction(direction || FORWARD_DIRECTION); - this._adjustCaret(key); - this._handleKeyChain(key); - this._moveCaret(); - }, - - _handleSelection: function() { - if(!this._hasSelection()) { - return; - } - - const caret = this._caret(); - const emptyChars = new Array(caret.end - caret.start + 1).join(EMPTY_CHAR); - this._handleKeyChain(emptyChars); - }, - - _handleKeyChain: function(chars) { - const caret = this._caret(); - const start = this.isForwardDirection() ? caret.start : caret.start - 1; - const end = this.isForwardDirection() ? caret.end : caret.end - 1; - const length = start === end ? 1 : end - start; - - this._handleChain({ text: chars, start: start, length: length }); - }, - - _tryMoveCaretBackward: function() { - this.setBackwardDirection(); - const currentCaret = this._caret().start; - this._adjustCaret(); - return !currentCaret || currentCaret !== this._caret().start; - }, - - _adjustCaret: function(char) { - const caretStart = this._caret().start; - const isForwardDirection = this.isForwardDirection(); - - const caret = this._maskRulesChain.adjustedCaret(caretStart, isForwardDirection, char); - this._caret({ start: caret, end: caret }); - }, - - _moveCaret: function() { - const currentCaret = this._caret().start; - const maskRuleIndex = currentCaret + (this.isForwardDirection() ? 0 : -1); - - const caret = this._maskRulesChain.isAccepted(maskRuleIndex) - ? currentCaret + (this.isForwardDirection() ? 1 : -1) - : currentCaret; - - this._caret({ start: caret, end: caret }); - }, - - _caret: function(position, force) { - const $input = this._input(); - - if(!$input.length) { - return; - } - - if(!arguments.length) { - return caret($input); - } - caret($input, position, force); - }, - - _hasSelection: function() { - const caret = this._caret(); - return caret.start !== caret.end; - }, - - _direction: function(direction) { - if(!arguments.length) { - return this._typingDirection; - } - - this._typingDirection = direction; - }, - - setForwardDirection: function() { - this._direction(FORWARD_DIRECTION); - }, - - setBackwardDirection: function() { - this._direction(BACKWARD_DIRECTION); - }, - - isForwardDirection: function() { - return this._direction() === FORWARD_DIRECTION; - }, - - _updateMaskInfo() { - this._textValue = this._maskRulesChain.text(); - this._value = this._maskRulesChain.value(); - }, - - _clean: function() { - this._maskStrategy && this._maskStrategy.clean(); - this.callBase(); - }, - - _validateMask: function() { - if(!this._maskRulesChain) { - return; - } - const isValid = isEmpty(this.option('value')) || this._maskRulesChain.isValid(this._normalizeChainArguments()); - - this.option({ - isValid: isValid, - validationError: isValid ? null : { editorSpecific: true, message: this.option('maskInvalidMessage') } - }); - }, - - _updateHiddenElement: function() { - this._removeHiddenElement(); - - if(this.option('mask')) { - this._input().removeAttr('name'); - this._renderHiddenElement(); - } - - this._setSubmitElementName(this.option('name')); - }, - - _updateMaskOption: function() { - this._updateHiddenElement(); - this._renderMask(); - this._validateMask(); - this._refreshValueChangeEvent(); - }, + const value = this.option('value') || ''; + this._maskRulesChain.clear(this._normalizeChainArguments()); - _processEmptyMask: function(mask) { - if(mask) return; + const chainArgs = { length: value.length }; + chainArgs[this._isMaskedValueMode() ? 'text' : 'value'] = value; - const value = this.option('value'); + this._handleChain(chainArgs); + this._displayMask(); + }, - this.option({ - text: value, - isValid: true, - validationError: null, - }); + _replaceSelectedText(text, selection, char) { + if (char === undefined) { + return text; + } + + const textBefore = text.slice(0, selection.start); + const textAfter = text.slice(selection.end); + const edited = textBefore + char + textAfter; + + return edited; + }, + + _isMaskedValueMode() { + return this.option('useMaskedValue'); + }, + + _displayMask(caret) { + caret = caret || this._caret(); + this._renderValue(); + this._caret(caret); + }, + + _isValueEmpty() { + return isEmpty(this._value); + }, + + _shouldShowMask() { + const showMaskMode = this.option('showMaskMode'); + + if (showMaskMode === 'onFocus') { + return focused(this._input()) || !this._isValueEmpty(); + } + + return true; + }, + + _showMaskPlaceholder() { + if (this._shouldShowMask()) { + const text = this._maskRulesChain.text(); + this.option('text', text); + if (this.option('showMaskMode') === 'onFocus') { + this._renderDisplayText(text); + } + } + }, - this.validationRequest.fire({ - value: value, - editor: this, - }); + _renderValue() { + if (this._maskRulesChain) { + this._showMaskPlaceholder(); + if (this._$hiddenElement) { + const value = this._maskRulesChain.value(); + const submitElementValue = !isEmpty(value) + ? this._getPreparedValue() + : ''; + + this._$hiddenElement.val(submitElementValue); + } + } + return this.callBase(); + }, + + _getPreparedValue() { + return this._convertToValue().replace(/\s+$/, ''); + }, + + _valueChangeEventHandler(e) { + if (!this._maskRulesChain) { + this.callBase.apply(this, arguments); + return; + } + + this._saveValueChangeEvent(e); + + this.option('value', this._getPreparedValue()); + }, + + _isControlKeyFired(e) { + return this._isControlKey(normalizeKeyName(e)) || isCommandKeyPressed(e); + }, + + _handleChain(args) { + const handledCount = this._maskRulesChain.handle(this._normalizeChainArguments(args)); + this._updateMaskInfo(); + return handledCount; + }, + + _normalizeChainArguments(args) { + args = args || {}; + args.index = 0; + args.fullText = this._maskRulesChain.text(); + return args; + }, + + _convertToValue(text) { + if (this._isMaskedValueMode()) { + text = this._replaceMaskCharWithEmpty(text || this._textValue || ''); + } else { + text = text || this._value || ''; + } + + return text; + }, + + _replaceMaskCharWithEmpty(text) { + return text.replace(new RegExp(this.option('maskChar'), 'g'), EMPTY_CHAR); + }, + + _maskKeyHandler(e, keyHandler) { + if (this.option('readOnly')) { + return; + } + + this.setForwardDirection(); + e.preventDefault(); + + this._handleSelection(); + + const previousText = this._input().val(); + const raiseInputEvent = () => { + if (previousText !== this._input().val()) { + // @ts-expect-error + eventsEngine.trigger(this._input(), 'input'); + } + }; + + const handled = keyHandler(); + + if (handled) { + handled.then(raiseInputEvent); + } else { + this.setForwardDirection(); + this._adjustCaret(); + this._displayMask(); + this._maskRulesChain.reset(); + raiseInputEvent(); + } + }, + + _handleKey(key, direction) { + this._direction(direction || FORWARD_DIRECTION); + this._adjustCaret(key); + this._handleKeyChain(key); + this._moveCaret(); + }, + + _handleSelection() { + if (!this._hasSelection()) { + return; + } + + const caret = this._caret(); + const emptyChars = new Array(caret.end - caret.start + 1).join(EMPTY_CHAR); + this._handleKeyChain(emptyChars); + }, + + _handleKeyChain(chars) { + const caret = this._caret(); + const start = this.isForwardDirection() ? caret.start : caret.start - 1; + const end = this.isForwardDirection() ? caret.end : caret.end - 1; + const length = start === end ? 1 : end - start; + + this._handleChain({ text: chars, start, length }); + }, + + _tryMoveCaretBackward() { + this.setBackwardDirection(); + const currentCaret = this._caret().start; + this._adjustCaret(); + return !currentCaret || currentCaret !== this._caret().start; + }, + + _adjustCaret(char) { + const caretStart = this._caret().start; + const isForwardDirection = this.isForwardDirection(); + + const caret = this._maskRulesChain.adjustedCaret(caretStart, isForwardDirection, char); + this._caret({ start: caret, end: caret }); + }, + + _moveCaret() { + const currentCaret = this._caret().start; + const maskRuleIndex = currentCaret + (this.isForwardDirection() ? 0 : -1); + + const caret = this._maskRulesChain.isAccepted(maskRuleIndex) + ? currentCaret + (this.isForwardDirection() ? 1 : -1) + : currentCaret; + + this._caret({ start: caret, end: caret }); + }, + + _caret(position, force) { + const $input = this._input(); + + if (!$input.length) { + return; + } + + if (!arguments.length) { + return caret($input); + } + caret($input, position, force); + }, + + _hasSelection() { + const caret = this._caret(); + return caret.start !== caret.end; + }, + + _direction(direction) { + if (!arguments.length) { + return this._typingDirection; + } + + this._typingDirection = direction; + }, + + setForwardDirection() { + this._direction(FORWARD_DIRECTION); + }, + + setBackwardDirection() { + this._direction(BACKWARD_DIRECTION); + }, + + isForwardDirection() { + return this._direction() === FORWARD_DIRECTION; + }, + + _updateMaskInfo() { + this._textValue = this._maskRulesChain.text(); + this._value = this._maskRulesChain.value(); + }, + + _clean() { + this._maskStrategy && this._maskStrategy.clean(); + this.callBase(); + }, + + _validateMask() { + if (!this._maskRulesChain) { + return; + } + const isValid = isEmpty(this.option('value')) || this._maskRulesChain.isValid(this._normalizeChainArguments()); + + this.option({ + isValid, + validationError: isValid ? null : { editorSpecific: true, message: this.option('maskInvalidMessage') }, + }); + }, + + _updateHiddenElement() { + this._removeHiddenElement(); + + if (this.option('mask')) { + this._input().removeAttr('name'); + this._renderHiddenElement(); + } + + this._setSubmitElementName(this.option('name')); + }, + + _updateMaskOption() { + this._updateHiddenElement(); + this._renderMask(); + this._validateMask(); + this._refreshValueChangeEvent(); + }, + + _processEmptyMask(mask) { + if (mask) return; + + const value = this.option('value'); + + this.option({ + text: value, + isValid: true, + validationError: null, + }); + + this.validationRequest.fire({ + value, + editor: this, + }); + + this._renderValue(); + }, + + _optionChanged(args) { + switch (args.name) { + case 'mask': + this._updateMaskOption(); + this._processEmptyMask(args.value); + break; + case 'maskChar': + case 'maskRules': + case 'useMaskedValue': + this._updateMaskOption(); + break; + case 'value': + this._renderMaskedValue(); + this._validateMask(); + this.callBase(args); + + this._changedValue = this._input().val(); + break; + case 'maskInvalidMessage': + break; + case 'showMaskMode': + this.option('text', ''); this._renderValue(); - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'mask': - this._updateMaskOption(); - this._processEmptyMask(args.value); - break; - case 'maskChar': - case 'maskRules': - case 'useMaskedValue': - this._updateMaskOption(); - break; - case 'value': - this._renderMaskedValue(); - this._validateMask(); - this.callBase(args); - - this._changedValue = this._input().val(); - break; - case 'maskInvalidMessage': - break; - case 'showMaskMode': - this.option('text', ''); - this._renderValue(); - break; - default: - this.callBase(args); - } - }, - - clear: function() { - const { value: defaultValue } = this._getDefaultOptions(); - if(this.option('value') === defaultValue) { - this._renderMaskedValue(); - } - this.callBase(); + break; + default: + this.callBase(args); + } + }, + + clear() { + const { value: defaultValue } = this._getDefaultOptions(); + if (this.option('value') === defaultValue) { + this._renderMaskedValue(); } + this.callBase(); + }, }); export default TextEditorMask; diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.ts index 0624a16fa021..1d1b29d16ad0 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.ts @@ -1,5 +1,6 @@ -import registerComponent from '../../core/component_registrator'; -import TextEditorMask from './ui.text_editor.mask'; +import registerComponent from '@js/core/component_registrator'; + +import TextEditorMask from './m_text_editor.mask'; registerComponent('dxTextEditor', TextEditorMask); diff --git a/packages/devextreme/js/__internal/ui/text_box/m_utils.caret.ts b/packages/devextreme/js/__internal/ui/text_box/m_utils.caret.ts index a43f73e3156f..e6596123a28d 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_utils.caret.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_utils.caret.ts @@ -1,54 +1,59 @@ -import $ from '../../core/renderer'; -import { isDefined } from '../../core/utils/type'; -import devices from '../../core/devices'; -import domAdapter from '../../core/dom_adapter'; - -const { ios, mac } = devices.real(); +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import $ from '@js/core/renderer'; +import { isDefined } from '@js/core/utils/type'; + +const { + ios, + // @ts-expect-error + mac, +} = devices.real(); +// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isFocusingOnCaretChange = ios || mac; -const getCaret = function(input) { - let range; - - try { - range = { - start: input.selectionStart, - end: input.selectionEnd - }; - } catch(e) { - range = { - start: 0, - end: 0 - }; - } - - return range; +const getCaret = function (input) { + let range; + + try { + range = { + start: input.selectionStart, + end: input.selectionEnd, + }; + } catch (e) { + range = { + start: 0, + end: 0, + }; + } + + return range; }; -const setCaret = function(input, position) { - const body = domAdapter.getBody(); - if(!body.contains(input) && !body.contains(input.getRootNode().host)) { - return; - } +const setCaret = function (input, position) { + const body = domAdapter.getBody(); + if (!body.contains(input) && !body.contains(input.getRootNode().host)) { + return; + } - try { - input.selectionStart = position.start; - input.selectionEnd = position.end; - } catch(e) { } + try { + input.selectionStart = position.start; + input.selectionEnd = position.end; + } catch (e) { /* empty */ } }; -const caret = function(input, position, force = false) { - input = $(input).get(0); +const caret = function (input, position?: any, force = false) { + input = $(input).get(0); - if(!isDefined(position)) { - return getCaret(input); - } + if (!isDefined(position)) { + return getCaret(input); + } - // NOTE: AppleWebKit-based browsers focuses element input after caret position has changed - if(!force && isFocusingOnCaretChange && domAdapter.getActiveElement(input) !== input) { - return; - } + // NOTE: AppleWebKit-based browsers focuses element input after caret position has changed + if (!force && isFocusingOnCaretChange && domAdapter.getActiveElement(input) !== input) { + return; + } - setCaret(input, position); + setCaret(input, position); }; export default caret; diff --git a/packages/devextreme/js/__internal/ui/text_box/m_utils.scroll.ts b/packages/devextreme/js/__internal/ui/text_box/m_utils.scroll.ts index 219eeace642b..a764ecf48c09 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_utils.scroll.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_utils.scroll.ts @@ -1,49 +1,55 @@ -import $ from '../../core/renderer'; -import { isDxMouseWheelEvent } from '../../events/utils/index'; - -const allowScroll = function(container, delta, shiftKey) { - const $container = $(container); - const scrollTopPos = shiftKey ? $container.scrollLeft() : $container.scrollTop(); - - const prop = shiftKey ? 'Width' : 'Height'; - const scrollSize = $container.prop(`scroll${prop}`); - const clientSize = $container.prop(`client${prop}`); - // NOTE: round to the nearest integer towards zero - const scrollBottomPos = (scrollSize - clientSize - scrollTopPos) | 0; - - if(scrollTopPos === 0 && scrollBottomPos === 0) { - return false; - } - - const isScrollFromTop = scrollTopPos === 0 && delta >= 0; - const isScrollFromBottom = scrollBottomPos === 0 && delta <= 0; - const isScrollFromMiddle = scrollTopPos > 0 && scrollBottomPos > 0; - - if(isScrollFromTop || isScrollFromBottom || isScrollFromMiddle) { - return true; - } +import $ from '@js/core/renderer'; +import { isDxMouseWheelEvent } from '@js/events/utils/index'; + +// @ts-expect-error +const allowScroll = function (container, delta, shiftKey?: boolean) { + const $container = $(container); + const scrollTopPos = shiftKey ? $container.scrollLeft() : $container.scrollTop(); + + const prop = shiftKey ? 'Width' : 'Height'; + // @ts-expect-error + const scrollSize = $container.prop(`scroll${prop}`); + // @ts-expect-error + const clientSize = $container.prop(`client${prop}`); + // @ts-expect-error + // NOTE: round to the nearest integer towards zero + const scrollBottomPos = (scrollSize - clientSize - scrollTopPos) | 0; + // @ts-expect-error + if (scrollTopPos === 0 && scrollBottomPos === 0) { + return false; + } + // @ts-expect-error + const isScrollFromTop = scrollTopPos === 0 && delta >= 0; + const isScrollFromBottom = scrollBottomPos === 0 && delta <= 0; + // @ts-expect-error + const isScrollFromMiddle = scrollTopPos > 0 && scrollBottomPos > 0; + + if (isScrollFromTop || isScrollFromBottom || isScrollFromMiddle) { + return true; + } }; -const prepareScrollData = function(container, validateTarget) { - const $container = $(container); - const isCorrectTarget = function(eventTarget) { - return validateTarget ? $(eventTarget).is(container) : true; - }; - - return { - validate: function(e) { - if(isDxMouseWheelEvent(e) && isCorrectTarget(e.target)) { - if(allowScroll($container, -e.delta, e.shiftKey)) { - e._needSkipEvent = true; - return true; - } - return false; - } +const prepareScrollData = function (container, validateTarget) { + const $container = $(container); + const isCorrectTarget = function (eventTarget) { + return validateTarget ? $(eventTarget).is(container) : true; + }; + + return { + // @ts-expect-error + validate(e) { + if (isDxMouseWheelEvent(e) && isCorrectTarget(e.target)) { + if (allowScroll($container, -e.delta, e.shiftKey)) { + e._needSkipEvent = true; + return true; } - }; + return false; + } + }, + }; }; export { - allowScroll, - prepareScrollData + allowScroll, + prepareScrollData, }; diff --git a/packages/devextreme/js/ui/html_editor/ui.html_editor.js b/packages/devextreme/js/ui/html_editor/ui.html_editor.js index b4c3531d7030..e19a3271e061 100644 --- a/packages/devextreme/js/ui/html_editor/ui.html_editor.js +++ b/packages/devextreme/js/ui/html_editor/ui.html_editor.js @@ -13,7 +13,7 @@ import eventsEngine from '../../events/core/events_engine'; import { addNamespace } from '../../events/utils/index'; import { Event as dxEvent } from '../../events/index'; import scrollEvents from '../../events/gesture/emitter.gesture.scroll'; -import { prepareScrollData } from '../text_box/utils.scroll'; +import { prepareScrollData } from '../../__internal/ui/text_box/m_utils.scroll'; import pointerEvents from '../../events/pointer'; import devices from '../../core/devices'; diff --git a/packages/devextreme/js/ui/text_box/text_box.js b/packages/devextreme/js/ui/text_box/text_box.js new file mode 100644 index 000000000000..657ca0f4ccbc --- /dev/null +++ b/packages/devextreme/js/ui/text_box/text_box.js @@ -0,0 +1,5 @@ +import TextBox from '../../__internal/ui/text_box/m_text_box'; + +export default TextBox; + +// STYLE textBox diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.base.js b/packages/devextreme/js/ui/text_box/ui.text_editor.base.js new file mode 100644 index 000000000000..20547b4ca1c7 --- /dev/null +++ b/packages/devextreme/js/ui/text_box/ui.text_editor.base.js @@ -0,0 +1,3 @@ +import TextEditorBase from '../../__internal/ui/text_box/m_text_editor.base'; + +export default TextEditorBase; diff --git a/packages/devextreme/js/ui/text_box/ui.text_editor.js b/packages/devextreme/js/ui/text_box/ui.text_editor.js new file mode 100644 index 000000000000..94ae9b32b803 --- /dev/null +++ b/packages/devextreme/js/ui/text_box/ui.text_editor.js @@ -0,0 +1,3 @@ +import TextEditorMask from '../../__internal/ui/text_box/m_text_editor'; + +export default TextEditorMask; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js index e0ac12ceba5f..8cc3281d08a5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js @@ -27,7 +27,7 @@ import pointerMock from '../../helpers/pointerMock.js'; import keyboardMock from '../../helpers/keyboardMock.js'; import ariaAccessibilityTestHelper from '../../helpers/ariaAccessibilityTestHelper.js'; -import { TextEditorLabel } from 'ui/text_box/ui.text_editor.label.js'; +import { TextEditorLabel } from '__internal/ui/text_box/m_text_editor.label'; import 'generic_light.css!'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js index 74a700e7fc4f..42a5c33fee92 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js @@ -21,7 +21,7 @@ import { normalizeKeyName } from 'events/utils/index'; import { getWidth, getHeight } from 'core/utils/size'; import Guid from 'core/guid'; -import { TextEditorLabel } from 'ui/text_box/ui.text_editor.label.js'; +import { TextEditorLabel } from '__internal/ui/text_box/m_text_editor.label'; import 'generic_light.css!'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorLabel.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorLabel.tests.js index a16be7893c80..da51257d20e5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorLabel.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorLabel.tests.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { TextEditorLabel } from 'ui/text_box/ui.text_editor.label'; +import { TextEditorLabel } from '__internal/ui/text_box/m_text_editor.label'; import { getWidth } from 'core/utils/size'; const TEXTEDITOR_LABEL_CLASS = 'dx-texteditor-label'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/caretWorkaround.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/caretWorkaround.js index 627d1e3af8f4..918bee836d83 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/caretWorkaround.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/caretWorkaround.js @@ -1,5 +1,5 @@ import browser from 'core/utils/browser'; -import caret from 'ui/text_box/utils.caret'; +import caret from '__internal/ui/text_box/m_utils.caret'; export default function($input) { const isDesktopChrome = ('chrome' in window) && ('app' in window.chrome) && ('isInstalled' in window.chrome.app); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js index a71a43a160db..1edd977a39d9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js @@ -12,7 +12,7 @@ import { normalizeKeyName } from 'events/utils/index'; import { getWidth, implementationsMap } from 'core/utils/size'; import TextEditor from 'ui/text_box/ui.text_editor'; -import { TextEditorLabel } from 'ui/text_box/ui.text_editor.label.js'; +import { TextEditorLabel } from '__internal/ui/text_box/m_text_editor.label'; const TEXTEDITOR_CLASS = 'dx-texteditor'; const INPUT_CLASS = 'dx-texteditor-input'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textbox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textbox.tests.js index 301ec663cacd..c189dec3e772 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textbox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textbox.tests.js @@ -5,7 +5,7 @@ import executeAsyncMock from '../../helpers/executeAsyncMock.js'; import { getWidth, getOuterWidth } from 'core/utils/size'; import keyboardMock from '../../helpers/keyboardMock.js'; -import { TextEditorLabel } from 'ui/text_box/ui.text_editor.label.js'; +import { TextEditorLabel } from '__internal/ui/text_box/m_text_editor.label'; import 'generic_light.css!'; diff --git a/packages/devextreme/testing/tests/DevExpress.utils/utils.caret.tests.js b/packages/devextreme/testing/tests/DevExpress.utils/utils.caret.tests.js index d8db7d5f3891..8a0e994b9744 100644 --- a/packages/devextreme/testing/tests/DevExpress.utils/utils.caret.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.utils/utils.caret.tests.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import caret from 'ui/text_box/utils.caret'; +import caret from '__internal/ui/text_box/m_utils.caret'; import keyboardMock from '../../helpers/keyboardMock.js'; import domAdapter from 'core/dom_adapter'; import devices from 'core/devices'; From 420bef9e5bddf07d7d5d3a0eb3d50021ef4372c6 Mon Sep 17 00:00:00 2001 From: Alexander Bulychev Date: Wed, 12 Jun 2024 16:39:24 +0400 Subject: [PATCH 24/32] CI: save all demos frameworks screenshots into single archive (#27566) Co-authored-by: Alexander Bulychev --- .github/workflows/demos_visual_tests_frameworks.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/demos_visual_tests_frameworks.yml b/.github/workflows/demos_visual_tests_frameworks.yml index 1be94efa97fa..6ba80d6562a2 100644 --- a/.github/workflows/demos_visual_tests_frameworks.yml +++ b/.github/workflows/demos_visual_tests_frameworks.yml @@ -384,15 +384,10 @@ jobs: CI_ENV: true # The `ignore` field in the visualtestrc.json should be disabled when running test locally run: npx nx test-testcafe - - name: Get screenshots artifacts name - if: ${{ failure() }} - id: screenshotname - run: echo "value=$(echo "${{ matrix.CONSTEL }}" | grep -oP "^\w+")" >> $GITHUB_OUTPUT - - name: Copy screenshots artifacts if: ${{ failure() }} uses: actions/upload-artifact@v3 with: - name: screenshots-${{ steps.screenshotname.outputs.value }}-${{ matrix.THEME }} + name: screenshots path: ${{ github.workspace }}/apps/demos/testing/artifacts/compared-screenshots/**/* if-no-files-found: ignore From 9e6e4fd0d3e3af02ad3dabf5d93f70adefa1f406 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 11:18:23 +0400 Subject: [PATCH 25/32] Popup, Popover: move files to TS --- .../ui/popover/m_popover.full.ts} | 0 .../popover/ui.popover.js => __internal/ui/popover/m_popover.ts} | 0 .../ui/popover/m_popover_position_controller.ts} | 0 .../ui.popup.full.js => __internal/ui/popup/m_popup.full.ts} | 0 .../js/{ui/popup/ui.popup.js => __internal/ui/popup/m_popup.ts} | 0 .../popup/popup_drag.js => __internal/ui/popup/m_popup_drag.ts} | 0 .../ui/popup/m_popup_overflow_manager.ts} | 0 .../ui/popup/m_popup_position_controller.ts} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/popover/ui.popover.full.js => __internal/ui/popover/m_popover.full.ts} (100%) rename packages/devextreme/js/{ui/popover/ui.popover.js => __internal/ui/popover/m_popover.ts} (100%) rename packages/devextreme/js/{ui/popover/popover_position_controller.js => __internal/ui/popover/m_popover_position_controller.ts} (100%) rename packages/devextreme/js/{ui/popup/ui.popup.full.js => __internal/ui/popup/m_popup.full.ts} (100%) rename packages/devextreme/js/{ui/popup/ui.popup.js => __internal/ui/popup/m_popup.ts} (100%) rename packages/devextreme/js/{ui/popup/popup_drag.js => __internal/ui/popup/m_popup_drag.ts} (100%) rename packages/devextreme/js/{ui/popup/popup_overflow_manager.js => __internal/ui/popup/m_popup_overflow_manager.ts} (100%) rename packages/devextreme/js/{ui/popup/popup_position_controller.js => __internal/ui/popup/m_popup_position_controller.ts} (100%) diff --git a/packages/devextreme/js/ui/popover/ui.popover.full.js b/packages/devextreme/js/__internal/ui/popover/m_popover.full.ts similarity index 100% rename from packages/devextreme/js/ui/popover/ui.popover.full.js rename to packages/devextreme/js/__internal/ui/popover/m_popover.full.ts diff --git a/packages/devextreme/js/ui/popover/ui.popover.js b/packages/devextreme/js/__internal/ui/popover/m_popover.ts similarity index 100% rename from packages/devextreme/js/ui/popover/ui.popover.js rename to packages/devextreme/js/__internal/ui/popover/m_popover.ts diff --git a/packages/devextreme/js/ui/popover/popover_position_controller.js b/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts similarity index 100% rename from packages/devextreme/js/ui/popover/popover_position_controller.js rename to packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts diff --git a/packages/devextreme/js/ui/popup/ui.popup.full.js b/packages/devextreme/js/__internal/ui/popup/m_popup.full.ts similarity index 100% rename from packages/devextreme/js/ui/popup/ui.popup.full.js rename to packages/devextreme/js/__internal/ui/popup/m_popup.full.ts diff --git a/packages/devextreme/js/ui/popup/ui.popup.js b/packages/devextreme/js/__internal/ui/popup/m_popup.ts similarity index 100% rename from packages/devextreme/js/ui/popup/ui.popup.js rename to packages/devextreme/js/__internal/ui/popup/m_popup.ts diff --git a/packages/devextreme/js/ui/popup/popup_drag.js b/packages/devextreme/js/__internal/ui/popup/m_popup_drag.ts similarity index 100% rename from packages/devextreme/js/ui/popup/popup_drag.js rename to packages/devextreme/js/__internal/ui/popup/m_popup_drag.ts diff --git a/packages/devextreme/js/ui/popup/popup_overflow_manager.js b/packages/devextreme/js/__internal/ui/popup/m_popup_overflow_manager.ts similarity index 100% rename from packages/devextreme/js/ui/popup/popup_overflow_manager.js rename to packages/devextreme/js/__internal/ui/popup/m_popup_overflow_manager.ts diff --git a/packages/devextreme/js/ui/popup/popup_position_controller.js b/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts similarity index 100% rename from packages/devextreme/js/ui/popup/popup_position_controller.js rename to packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts From 960767b962efc1c7790f71b2262177344bc3f039 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 12:21:52 +0400 Subject: [PATCH 26/32] Popup, Popover: ignore errors after move to TS --- .../pivotGrid/virtualScrolling_T1210807.ts | 2 +- .../js/__internal/ui/menu/m_menu.ts | 2 +- .../__internal/ui/popover/m_popover.full.ts | 34 +- .../js/__internal/ui/popover/m_popover.ts | 934 ++++---- .../popover/m_popover_position_controller.ts | 226 +- .../js/__internal/ui/popup/m_popup.full.ts | 33 +- .../js/__internal/ui/popup/m_popup.ts | 1878 +++++++++-------- .../js/__internal/ui/popup/m_popup_drag.ts | 415 ++-- .../ui/popup/m_popup_overflow_manager.ts | 273 +-- .../ui/popup/m_popup_position_controller.ts | 201 +- .../m_slider_tooltip_position_controller.ts | 2 +- .../speed_dial_action/m_speed_dial_action.ts | 2 - .../js/ui/popover/ui.popover.full.js | 3 + .../devextreme/js/ui/popover/ui.popover.js | 78 + .../devextreme/js/ui/popup/ui.popup.full.js | 3 + packages/devextreme/js/ui/popup/ui.popup.js | 5 + packages/devextreme/renovation.tsconfig.json | 1 + .../DevExpress.ui.widgets/popupDrag.tests.js | 4 +- 18 files changed, 2094 insertions(+), 2002 deletions(-) create mode 100644 packages/devextreme/js/ui/popover/ui.popover.full.js create mode 100644 packages/devextreme/js/ui/popover/ui.popover.js create mode 100644 packages/devextreme/js/ui/popup/ui.popup.full.js create mode 100644 packages/devextreme/js/ui/popup/ui.popup.js diff --git a/e2e/testcafe-devextreme/tests/pivotGrid/virtualScrolling_T1210807.ts b/e2e/testcafe-devextreme/tests/pivotGrid/virtualScrolling_T1210807.ts index ac0dd55c8395..86a6b7af473f 100644 --- a/e2e/testcafe-devextreme/tests/pivotGrid/virtualScrolling_T1210807.ts +++ b/e2e/testcafe-devextreme/tests/pivotGrid/virtualScrolling_T1210807.ts @@ -32,7 +32,7 @@ const createData = (count, innerCount) => { return result; }; -test('Row fields overlap data fields if dataFieldArea is set to "row" and virtual scrolling is enabled (T1210807)', async (t) => { +test.meta({ unstable: true })('Row fields overlap data fields if dataFieldArea is set to "row" and virtual scrolling is enabled (T1210807)', async (t) => { const { takeScreenshot, compareResults } = createScreenshotsComparer(t); const pivotGrid = new PivotGrid('#container'); const firstHeaderRow = pivotGrid.getRowsArea(2).getCell(0); diff --git a/packages/devextreme/js/__internal/ui/menu/m_menu.ts b/packages/devextreme/js/__internal/ui/menu/m_menu.ts index 09a341f9ea83..dd5ca792006e 100644 --- a/packages/devextreme/js/__internal/ui/menu/m_menu.ts +++ b/packages/devextreme/js/__internal/ui/menu/m_menu.ts @@ -449,7 +449,7 @@ class Menu extends MenuBase { const $hamburger = this._renderHamburgerButton(); this._treeView = this._createComponent($('
'), TreeView, this._getTreeViewOptions()); - + // @ts-expect-error this._overlay = this._createComponent($('
'), Overlay, this._getAdaptiveOverlayOptions()); // @ts-expect-error this._overlay.$content() diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.full.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.full.ts index 406e7657b962..ea699787085d 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.full.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.full.ts @@ -1,21 +1,23 @@ -import '../toolbar'; -import Popover from '../popover/ui.popover'; -import registerComponent from '../../core/component_registrator'; -import { extend } from '../../core/utils/extend'; +import '@js/ui/toolbar'; + +import registerComponent from '@js/core/component_registrator'; +import { extend } from '@js/core/utils/extend'; +import Popover from '@js/ui/popover/ui.popover'; + export default class PopoverFull extends Popover { - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - preventScrollEvents: false, - }); - } + _getDefaultOptions() { + return extend(super._getDefaultOptions(), { + preventScrollEvents: false, + }); + } - _getToolbarName() { - return 'dxToolbar'; - } + _getToolbarName() { + return 'dxToolbar'; + } } - -PopoverFull.defaultOptions = function(rule) { - Popover.defaultOptions(rule); +// @ts-expect-error +PopoverFull.defaultOptions = function (rule) { + Popover.defaultOptions(rule); }; - +// @ts-expect-error registerComponent('dxPopover', PopoverFull); diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.ts index 2cc2ec977a4e..a44645739713 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.ts @@ -1,23 +1,24 @@ -import { getWidth, setWidth, getHeight, setHeight } from '../../core/utils/size'; -import $ from '../../core/renderer'; -import { hasWindow } from '../../core/utils/window'; -import { getPublicElement } from '../../core/element'; -import domAdapter from '../../core/dom_adapter'; -import eventsEngine from '../../events/core/events_engine'; -import registerComponent from '../../core/component_registrator'; -import { extend } from '../../core/utils/extend'; -import { move } from '../../animation/translator'; -import positionUtils from '../../animation/position'; -import { isObject, isString } from '../../core/utils/type'; -import { fitIntoRange } from '../../core/utils/math'; -import { addNamespace } from '../../events/utils/index'; -import errors from '../widget/ui.errors'; -import Popup from '../popup/ui.popup'; -import { getBoundingRect } from '../../core/utils/position'; -import { isMaterialBased, isMaterial } from '../themes'; -import { PopoverPositionController, POPOVER_POSITION_ALIASES } from './popover_position_controller'; - -// STYLE popover +import positionUtils from '@js/animation/position'; +import { move } from '@js/animation/translator'; +import registerComponent from '@js/core/component_registrator'; +import domAdapter from '@js/core/dom_adapter'; +import { getPublicElement } from '@js/core/element'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { fitIntoRange } from '@js/core/utils/math'; +import { getBoundingRect } from '@js/core/utils/position'; +import { + getHeight, getWidth, setHeight, setWidth, +} from '@js/core/utils/size'; +import { isObject, isString } from '@js/core/utils/type'; +import { hasWindow } from '@js/core/utils/window'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace } from '@js/events/utils/index'; +import Popup from '@js/ui/popup/ui.popup'; +import { isMaterial, isMaterialBased } from '@js/ui/themes'; +import errors from '@js/ui/widget/ui.errors'; + +import { POPOVER_POSITION_ALIASES, PopoverPositionController } from './m_popover_position_controller'; const POPOVER_CLASS = 'dx-popover'; const POPOVER_WRAPPER_CLASS = 'dx-popover-wrapper'; @@ -25,526 +26,453 @@ const POPOVER_ARROW_CLASS = 'dx-popover-arrow'; const POPOVER_WITHOUT_TITLE_CLASS = 'dx-popover-without-title'; const POSITION_FLIP_MAP = { - 'left': 'right', - 'top': 'bottom', - 'right': 'left', - 'bottom': 'top', - 'center': 'center' + left: 'right', + top: 'bottom', + right: 'left', + bottom: 'top', + center: 'center', }; -const getEventNameByOption = function(optionValue) { - return isObject(optionValue) ? optionValue.name : optionValue; +const getEventNameByOption = function (optionValue) { + // @ts-expect-error + return isObject(optionValue) ? optionValue.name : optionValue; }; -const getEventName = function(that, optionName) { - const optionValue = that.option(optionName); +const getEventName = function (that, optionName) { + const optionValue = that.option(optionName); - return getEventNameByOption(optionValue); + return getEventNameByOption(optionValue); }; -const getEventDelay = function(that, optionName) { - const optionValue = that.option(optionName); - - return isObject(optionValue) && optionValue.delay; +const getEventDelay = function (that, optionName) { + const optionValue = that.option(optionName); + // @ts-expect-error + return isObject(optionValue) && optionValue.delay; }; -const attachEvent = function(that, name) { - const { target, shading, disabled, hideEvent } = that.option(); - const isSelector = isString(target); - const shouldIgnoreHideEvent = shading && name === 'hide'; - const event = shouldIgnoreHideEvent ? null : getEventName(that, `${name}Event`); - - if(shouldIgnoreHideEvent && hideEvent) { - errors.log('W1020'); - } - - if(!event || disabled) { - return; - } - - const eventName = addNamespace(event, that.NAME); - const action = that._createAction((function() { - const delay = getEventDelay(that, name + 'Event'); - this._clearEventsTimeouts(); - - if(delay) { - this._timeouts[name] = setTimeout(function() { - that[name](); - }, delay); - } else { - that[name](); - } - }).bind(that), { validatingTargetName: 'target' }); - - const handler = function(e) { - action({ event: e, target: $(e.currentTarget) }); - }; - - const EVENT_HANDLER_NAME = '_' + name + 'EventHandler'; - if(isSelector) { - that[EVENT_HANDLER_NAME] = handler; - eventsEngine.on(domAdapter.getDocument(), eventName, target, handler); +const attachEvent = function (that, name) { + const { + target, shading, disabled, hideEvent, + } = that.option(); + const isSelector = isString(target); + const shouldIgnoreHideEvent = shading && name === 'hide'; + const event = shouldIgnoreHideEvent ? null : getEventName(that, `${name}Event`); + + if (shouldIgnoreHideEvent && hideEvent) { + errors.log('W1020'); + } + + if (!event || disabled) { + return; + } + + const eventName = addNamespace(event, that.NAME); + const action = that._createAction(function () { + const delay = getEventDelay(that, `${name}Event`); + this._clearEventsTimeouts(); + + if (delay) { + this._timeouts[name] = setTimeout(() => { + that[name](); + }, delay); } else { - const targetElement = getPublicElement($(target)); - that[EVENT_HANDLER_NAME] = undefined; - eventsEngine.on(targetElement, eventName, handler); + that[name](); } + }.bind(that), { validatingTargetName: 'target' }); + + const handler = function (e) { + action({ event: e, target: $(e.currentTarget) }); + }; + + const EVENT_HANDLER_NAME = `_${name}EventHandler`; + if (isSelector) { + that[EVENT_HANDLER_NAME] = handler; + eventsEngine.on(domAdapter.getDocument(), eventName, target, handler); + } else { + const targetElement = getPublicElement($(target)); + that[EVENT_HANDLER_NAME] = undefined; + eventsEngine.on(targetElement, eventName, handler); + } }; -const detachEvent = function(that, target, name, event) { - let eventName = event || getEventName(that, name + 'Event'); - - if(!eventName) { - return; - } - - eventName = addNamespace(eventName, that.NAME); - - const EVENT_HANDLER_NAME = '_' + name + 'EventHandler'; - if(that[EVENT_HANDLER_NAME]) { - eventsEngine.off(domAdapter.getDocument(), eventName, target, that[EVENT_HANDLER_NAME]); - } else { - eventsEngine.off(getPublicElement($(target)), eventName); - } +const detachEvent = function (that, target, name, event?: unknown) { + let eventName = event || getEventName(that, `${name}Event`); + + if (!eventName) { + return; + } + + eventName = addNamespace(eventName, that.NAME); + + const EVENT_HANDLER_NAME = `_${name}EventHandler`; + if (that[EVENT_HANDLER_NAME]) { + // @ts-expect-error + eventsEngine.off(domAdapter.getDocument(), eventName, target, that[EVENT_HANDLER_NAME]); + } else { + eventsEngine.off(getPublicElement($(target)), eventName); + } }; - const Popover = Popup.inherit({ - _getDefaultOptions: function() { - return extend(this.callBase(), { - target: undefined, - - shading: false, - - position: extend({}, POPOVER_POSITION_ALIASES.bottom), - - hideOnOutsideClick: true, - - animation: { - show: { - type: 'fade', - from: 0, - to: 1 - }, - hide: { - type: 'fade', - from: 1, - to: 0 - } - }, - - showTitle: false, - - width: 'auto', - - height: 'auto', - - /** - * @name dxPopoverOptions.dragEnabled - * @hidden - */ - dragEnabled: false, - - /** - * @name dxPopoverOptions.dragOutsideBoundary - * @hidden - */ - - /** - * @name dxPopoverOptions.dragAndResizeArea - * @hidden - */ - - /** - * @name dxPopoverOptions.resizeEnabled - * @hidden - */ - resizeEnabled: false, - - /** - * @name dxPopoverOptions.restorePosition - * @hidden - */ - - /** - * @name dxPopoverOptions.onResizeStart - * @hidden - */ - - /** - * @name dxPopoverOptions.onResize - * @hidden - */ - - /** - * @name dxPopoverOptions.onResizeEnd - * @hidden - */ - - /** - * @name dxPopoverOptions.fullScreen - * @hidden - */ - - fullScreen: false, - hideOnParentScroll: true, - arrowPosition: '', - arrowOffset: 0, - - _fixWrapperPosition: true - - /** - * @name dxPopoverOptions.focusStateEnabled - * @hidden - */ - - /** - * @name dxPopoverOptions.accessKey - * @hidden - */ - - /** - * @name dxPopoverOptions.tabIndex - * @hidden - */ - }); - }, - - _defaultOptionsRules: function() { - return [ - { - device: { platform: 'ios' }, - options: { - arrowPosition: { - boundaryOffset: { h: 20, v: -10 }, - collision: 'fit' - } - } - }, { - device: function() { - return !hasWindow(); - }, - options: { - animation: null - } - }, - { - device: function() { - return isMaterialBased(); - }, - options: { - useFlatToolbarButtons: true, - } - }, - { - device: function() { - return isMaterial(); - }, - options: { - useDefaultToolbarButtons: true, - showCloseButton: false - } - }, - ]; - }, - - _init: function() { - this.callBase(); - - this._renderArrow(); - this._timeouts = {}; - - this.$element().addClass(POPOVER_CLASS); - this.$wrapper().addClass(POPOVER_WRAPPER_CLASS); - - const isInteractive = this.option('toolbarItems')?.length; - this.setAria('role', isInteractive ? 'dialog' : 'tooltip'); - }, - - _render: function() { - this.callBase.apply(this, arguments); - this._detachEvents(this.option('target')); - this._attachEvents(); - }, - - _detachEvents: function(target) { - detachEvent(this, target, 'show'); - detachEvent(this, target, 'hide'); - }, - - _attachEvents: function() { - attachEvent(this, 'show'); - attachEvent(this, 'hide'); - }, - - _renderArrow: function() { - this._$arrow = $('
') - .addClass(POPOVER_ARROW_CLASS) - .prependTo(this.$overlayContent()); - }, - - _documentDownHandler: function(e) { - if(this._isOutsideClick(e)) { - return this.callBase(e); - } - return true; - }, - - _isOutsideClick: function(e) { - return !$(e.target).closest(this.option('target')).length; - }, - - _animate: function(animation) { - if(animation && animation.to && typeof animation.to === 'object') { - extend(animation.to, { - position: this._getContainerPosition() - }); - } - - this.callBase.apply(this, arguments); - }, - - _stopAnimation: function() { - this.callBase.apply(this, arguments); - }, + _getDefaultOptions() { + return extend(this.callBase(), { + target: undefined, + shading: false, + position: extend({}, POPOVER_POSITION_ALIASES.bottom), + hideOnOutsideClick: true, + animation: { + show: { + type: 'fade', + from: 0, + to: 1, + }, + hide: { + type: 'fade', + from: 1, + to: 0, + }, + }, + showTitle: false, + width: 'auto', + height: 'auto', + dragEnabled: false, + resizeEnabled: false, + fullScreen: false, + hideOnParentScroll: true, + arrowPosition: '', + arrowOffset: 0, + + _fixWrapperPosition: true, + }); + }, + + _defaultOptionsRules() { + return [ + { + device: { platform: 'ios' }, + options: { + arrowPosition: { + boundaryOffset: { h: 20, v: -10 }, + collision: 'fit', + }, + }, + }, { + device() { + return !hasWindow(); + }, + options: { + animation: null, + }, + }, + { + device(): boolean { + // @ts-expect-error + return isMaterialBased(); + }, + options: { + useFlatToolbarButtons: true, + }, + }, + { + device(): boolean { + // @ts-expect-error + return isMaterial(); + }, + options: { + useDefaultToolbarButtons: true, + showCloseButton: false, + }, + }, + ]; + }, + + _init() { + this.callBase(); + + this._renderArrow(); + this._timeouts = {}; + + this.$element().addClass(POPOVER_CLASS); + this.$wrapper().addClass(POPOVER_WRAPPER_CLASS); + + const isInteractive = this.option('toolbarItems')?.length; + this.setAria('role', isInteractive ? 'dialog' : 'tooltip'); + }, + + _render() { + this.callBase.apply(this, arguments); + this._detachEvents(this.option('target')); + this._attachEvents(); + }, + + _detachEvents(target) { + detachEvent(this, target, 'show'); + detachEvent(this, target, 'hide'); + }, + + _attachEvents() { + attachEvent(this, 'show'); + attachEvent(this, 'hide'); + }, + + _renderArrow() { + this._$arrow = $('
') + .addClass(POPOVER_ARROW_CLASS) + .prependTo(this.$overlayContent()); + }, + + _documentDownHandler(e) { + if (this._isOutsideClick(e)) { + return this.callBase(e); + } + return true; + }, + + _isOutsideClick(e) { + return !$(e.target).closest(this.option('target')).length; + }, + + _animate(animation) { + if (animation && animation.to && typeof animation.to === 'object') { + extend(animation.to, { + position: this._getContainerPosition(), + }); + } - _renderTitle: function() { - this.$wrapper().toggleClass(POPOVER_WITHOUT_TITLE_CLASS, !this.option('showTitle')); - this.callBase(); - }, + this.callBase.apply(this, arguments); + }, - _renderPosition: function(shouldUpdateDimensions = true) { - this.callBase(); - this._renderOverlayPosition(shouldUpdateDimensions); - this._actions.onPositioned(); - }, + _stopAnimation() { + this.callBase.apply(this, arguments); + }, - _renderOverlayPosition: function(shouldUpdateDimensions) { - this._resetOverlayPosition(shouldUpdateDimensions); - this._updateContentSize(shouldUpdateDimensions); + _renderTitle() { + this.$wrapper().toggleClass(POPOVER_WITHOUT_TITLE_CLASS, !this.option('showTitle')); + this.callBase(); + }, - const contentPosition = this._getContainerPosition(); - const resultLocation = positionUtils.setup(this.$overlayContent(), contentPosition); + _renderPosition(shouldUpdateDimensions = true) { + this.callBase(); + this._renderOverlayPosition(shouldUpdateDimensions); + this._actions.onPositioned(); + }, - const positionSide = this._getSideByLocation(resultLocation); + _renderOverlayPosition(shouldUpdateDimensions) { + this._resetOverlayPosition(shouldUpdateDimensions); + this._updateContentSize(shouldUpdateDimensions); - this._togglePositionClass('dx-position-' + positionSide); - this._toggleFlippedClass(resultLocation.h.flip, resultLocation.v.flip); + const contentPosition = this._getContainerPosition(); + // @ts-expect-error + const resultLocation = positionUtils.setup(this.$overlayContent(), contentPosition); - const isArrowVisible = this._isHorizontalSide() || this._isVerticalSide(); + const positionSide = this._getSideByLocation(resultLocation); - if(isArrowVisible) { - this._renderArrowPosition(positionSide); - } - }, + this._togglePositionClass(`dx-position-${positionSide}`); + this._toggleFlippedClass(resultLocation.h.flip, resultLocation.v.flip); - _resetOverlayPosition: function(shouldUpdateDimensions) { - this._setContentHeight(shouldUpdateDimensions); - this._togglePositionClass('dx-position-' + this._positionController._positionSide); + const isArrowVisible = this._isHorizontalSide() || this._isVerticalSide(); - move(this.$overlayContent(), { left: 0, top: 0 }); + if (isArrowVisible) { + this._renderArrowPosition(positionSide); + } + }, - this._$arrow.css({ - top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' - }); - }, + _resetOverlayPosition(shouldUpdateDimensions) { + this._setContentHeight(shouldUpdateDimensions); + this._togglePositionClass(`dx-position-${this._positionController._positionSide}`); - _updateContentSize: function(shouldUpdateDimensions) { - if(!this.$content() || !shouldUpdateDimensions) { - return; - } + move(this.$overlayContent(), { left: 0, top: 0 }); - const containerLocation = positionUtils.calculate(this.$overlayContent(), this._getContainerPosition()); + this._$arrow.css({ + top: 'auto', right: 'auto', bottom: 'auto', left: 'auto', + }); + }, - if((containerLocation.h.oversize > 0) && this._isHorizontalSide() && !containerLocation.h.fit) { - const newContainerWidth = getWidth(this.$overlayContent()) - containerLocation.h.oversize; + _updateContentSize(shouldUpdateDimensions) { + if (!this.$content() || !shouldUpdateDimensions) { + return; + } + // @ts-expect-error + const containerLocation = positionUtils.calculate(this.$overlayContent(), this._getContainerPosition()); - setWidth(this.$overlayContent(), newContainerWidth); - } + if ((containerLocation.h.oversize > 0) && this._isHorizontalSide() && !containerLocation.h.fit) { + const newContainerWidth = getWidth(this.$overlayContent()) - containerLocation.h.oversize; - if((containerLocation.v.oversize > 0) && this._isVerticalSide() && !containerLocation.v.fit) { - const newOverlayContentHeight = getHeight(this.$overlayContent()) - containerLocation.v.oversize; - const newPopupContentHeight = getHeight(this.$content()) - containerLocation.v.oversize; + setWidth(this.$overlayContent(), newContainerWidth); + } - setHeight(this.$overlayContent(), newOverlayContentHeight); - setHeight(this.$content(), newPopupContentHeight); - } - }, - - _getContainerPosition: function() { - return this._positionController._getContainerPosition(); - }, - - _getHideOnParentScrollTarget: function() { - return $(this._positionController._position.of || this.callBase()); - }, - - _getSideByLocation: function(location) { - const isFlippedByVertical = location.v.flip; - const isFlippedByHorizontal = location.h.flip; - - return (this._isVerticalSide() && isFlippedByVertical || this._isHorizontalSide() && isFlippedByHorizontal || this._isPopoverInside()) - ? POSITION_FLIP_MAP[this._positionController._positionSide] - : this._positionController._positionSide; - }, - - _togglePositionClass: function(positionClass) { - this.$wrapper() - .removeClass('dx-position-left dx-position-right dx-position-top dx-position-bottom') - .addClass(positionClass); - }, - - _toggleFlippedClass: function(isFlippedHorizontal, isFlippedVertical) { - this.$wrapper() - .toggleClass('dx-popover-flipped-horizontal', isFlippedHorizontal) - .toggleClass('dx-popover-flipped-vertical', isFlippedVertical); - }, - - _renderArrowPosition: function(side) { - const arrowRect = getBoundingRect(this._$arrow.get(0)); - const arrowFlip = -(this._isVerticalSide(side) ? arrowRect.height : arrowRect.width); - this._$arrow.css(POSITION_FLIP_MAP[side], arrowFlip); - - const axis = this._isVerticalSide(side) ? 'left' : 'top'; - const sizeProperty = this._isVerticalSide(side) ? 'width' : 'height'; - const $target = $(this._positionController._position.of); - - const targetOffset = positionUtils.offset($target) || { top: 0, left: 0 }; - const contentOffset = positionUtils.offset(this.$overlayContent()); - - const arrowSize = arrowRect[sizeProperty]; - const contentLocation = contentOffset[axis]; - const contentSize = getBoundingRect(this.$overlayContent().get(0))[sizeProperty]; - const targetLocation = targetOffset[axis]; - const targetElement = $target.get(0); - const targetSize = targetElement && !targetElement.preventDefault - ? getBoundingRect(targetElement)[sizeProperty] - : 0; - - const min = Math.max(contentLocation, targetLocation); - const max = Math.min(contentLocation + contentSize, targetLocation + targetSize); - let arrowLocation; - if(this.option('arrowPosition') === 'start') { - arrowLocation = min - contentLocation; - } else if(this.option('arrowPosition') === 'end') { - arrowLocation = max - contentLocation - arrowSize; - } else { - arrowLocation = (min + max) / 2 - contentLocation - arrowSize / 2; - } + if ((containerLocation.v.oversize > 0) && this._isVerticalSide() && !containerLocation.v.fit) { + const newOverlayContentHeight = getHeight(this.$overlayContent()) - containerLocation.v.oversize; + const newPopupContentHeight = getHeight(this.$content()) - containerLocation.v.oversize; - const borderWidth = this._positionController._getContentBorderWidth(side); - const finalArrowLocation = fitIntoRange(arrowLocation - borderWidth + this.option('arrowOffset'), borderWidth, contentSize - arrowSize - borderWidth * 2); - this._$arrow.css(axis, finalArrowLocation); - }, + setHeight(this.$overlayContent(), newOverlayContentHeight); + setHeight(this.$content(), newPopupContentHeight); + } + }, + + _getContainerPosition() { + return this._positionController._getContainerPosition(); + }, + + _getHideOnParentScrollTarget() { + return $(this._positionController._position.of || this.callBase()); + }, + + _getSideByLocation(location) { + const isFlippedByVertical = location.v.flip; + const isFlippedByHorizontal = location.h.flip; + + return this._isVerticalSide() && isFlippedByVertical || this._isHorizontalSide() && isFlippedByHorizontal || this._isPopoverInside() + ? POSITION_FLIP_MAP[this._positionController._positionSide] + : this._positionController._positionSide; + }, + + _togglePositionClass(positionClass) { + this.$wrapper() + .removeClass('dx-position-left dx-position-right dx-position-top dx-position-bottom') + .addClass(positionClass); + }, + + _toggleFlippedClass(isFlippedHorizontal, isFlippedVertical) { + this.$wrapper() + .toggleClass('dx-popover-flipped-horizontal', isFlippedHorizontal) + .toggleClass('dx-popover-flipped-vertical', isFlippedVertical); + }, + + _renderArrowPosition(side) { + const arrowRect = getBoundingRect(this._$arrow.get(0)); + const arrowFlip = -(this._isVerticalSide(side) ? arrowRect.height : arrowRect.width); + this._$arrow.css(POSITION_FLIP_MAP[side], arrowFlip); + + const axis = this._isVerticalSide(side) ? 'left' : 'top'; + const sizeProperty = this._isVerticalSide(side) ? 'width' : 'height'; + const $target = $(this._positionController._position.of); + // @ts-expect-error + const targetOffset = positionUtils.offset($target) || { top: 0, left: 0 }; + // @ts-expect-error + const contentOffset = positionUtils.offset(this.$overlayContent()); + + const arrowSize = arrowRect[sizeProperty]; + const contentLocation = contentOffset[axis]; + const contentSize = getBoundingRect(this.$overlayContent().get(0))[sizeProperty]; + const targetLocation = targetOffset[axis]; + const targetElement = $target.get(0); + // @ts-expect-error + const targetSize = targetElement && !targetElement.preventDefault + ? getBoundingRect(targetElement)[sizeProperty] + : 0; + + const min = Math.max(contentLocation, targetLocation); + const max = Math.min(contentLocation + contentSize, targetLocation + targetSize); + let arrowLocation; + if (this.option('arrowPosition') === 'start') { + arrowLocation = min - contentLocation; + } else if (this.option('arrowPosition') === 'end') { + arrowLocation = max - contentLocation - arrowSize; + } else { + arrowLocation = (min + max) / 2 - contentLocation - arrowSize / 2; + } - _isPopoverInside: function() { - return this._positionController._isPopoverInside(); - }, + const borderWidth = this._positionController._getContentBorderWidth(side); + const finalArrowLocation = fitIntoRange(arrowLocation - borderWidth + this.option('arrowOffset'), borderWidth, contentSize - arrowSize - borderWidth * 2); + this._$arrow.css(axis, finalArrowLocation); + }, - _setContentHeight: function(fullUpdate) { - if(fullUpdate) { - this.callBase(); - } - }, - - _getPositionControllerConfig() { - const { shading, target } = this.option(); - - return extend({}, this.callBase(), { - target, - shading, - $arrow: this._$arrow - }); - }, - - _initPositionController() { - this._positionController = new PopoverPositionController( - this._getPositionControllerConfig() - ); - }, - - _renderWrapperDimensions: function() { - if(this.option('shading')) { - this.$wrapper().css({ - width: '100%', - height: '100%' - }); - } - }, - - _isVerticalSide: function(side) { - return this._positionController._isVerticalSide(side); - }, - - _isHorizontalSide: function(side) { - return this._positionController._isHorizontalSide(side); - }, - - _clearEventTimeout: function(name) { - clearTimeout(this._timeouts[name]); - }, - - _clearEventsTimeouts: function() { - this._clearEventTimeout('show'); - this._clearEventTimeout('hide'); - }, - - _clean: function() { - this._detachEvents(this.option('target')); - this.callBase.apply(this, arguments); - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'arrowPosition': - case 'arrowOffset': - this._renderGeometry(); - break; - case 'fullScreen': - if(args.value) { - this.option('fullScreen', false); - } - break; - case 'target': - args.previousValue && this._detachEvents(args.previousValue); - this._positionController.updateTarget(args.value); - this._invalidate(); - break; - case 'showEvent': - case 'hideEvent': { - const name = args.name.substring(0, 4); - const event = getEventNameByOption(args.previousValue); - - this.hide(); - detachEvent(this, this.option('target'), name, event); - attachEvent(this, name); - break; - } - case 'visible': - this._clearEventTimeout(args.value ? 'show' : 'hide'); - this.callBase(args); - break; - default: - this.callBase(args); - } - }, + _isPopoverInside() { + return this._positionController._isPopoverInside(); + }, - show: function(target) { - if(target) { - this.option('target', target); + _setContentHeight(fullUpdate) { + if (fullUpdate) { + this.callBase(); + } + }, + + _getPositionControllerConfig() { + const { shading, target } = this.option(); + + return extend({}, this.callBase(), { + target, + shading, + $arrow: this._$arrow, + }); + }, + + _initPositionController() { + this._positionController = new PopoverPositionController( + this._getPositionControllerConfig(), + ); + }, + + _renderWrapperDimensions() { + if (this.option('shading')) { + this.$wrapper().css({ + width: '100%', + height: '100%', + }); + } + }, + + _isVerticalSide(side) { + return this._positionController._isVerticalSide(side); + }, + + _isHorizontalSide(side) { + return this._positionController._isHorizontalSide(side); + }, + + _clearEventTimeout(name) { + clearTimeout(this._timeouts[name]); + }, + + _clearEventsTimeouts() { + this._clearEventTimeout('show'); + this._clearEventTimeout('hide'); + }, + + _clean() { + this._detachEvents(this.option('target')); + this.callBase.apply(this, arguments); + }, + + _optionChanged(args) { + switch (args.name) { + case 'arrowPosition': + case 'arrowOffset': + this._renderGeometry(); + break; + case 'fullScreen': + if (args.value) { + this.option('fullScreen', false); } - - return this.callBase(); + break; + case 'target': + args.previousValue && this._detachEvents(args.previousValue); + this._positionController.updateTarget(args.value); + this._invalidate(); + break; + case 'showEvent': + case 'hideEvent': { + const name = args.name.substring(0, 4); + const event = getEventNameByOption(args.previousValue); + + this.hide(); + detachEvent(this, this.option('target'), name, event); + attachEvent(this, name); + break; + } + case 'visible': + this._clearEventTimeout(args.value ? 'show' : 'hide'); + this.callBase(args); + break; + default: + this.callBase(args); } + }, - /** - * @name dxPopover.registerKeyHandler - * @publicName registerKeyHandler(key, handler) - * @hidden - */ - - /** - * @name dxPopover.focus - * @publicName focus() - * @hidden - */ + show(target) { + if (target) { + this.option('target', target); + } + return this.callBase(); + }, }); registerComponent('dxPopover', Popover); diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts b/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts index d3d6c99054c3..6484d6ee1be0 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts @@ -1,137 +1,157 @@ -import { isDefined, isString } from '../../core/utils/type'; -import { extend } from '../../core/utils/extend'; -import positionUtils from '../../animation/position'; -import { pairToObject } from '../../core/utils/common'; -import { borderWidthStyles } from '../../renovation/ui/resizable/utils'; -import { getWidth, getHeight } from '../../core/utils/size'; -import { OverlayPositionController } from '../../__internal/ui/overlay/m_overlay_position_controller'; +import positionUtils from '@js/animation/position'; +import type { dxElementWrapper } from '@js/core/renderer'; +// @ts-expect-error +import { pairToObject } from '@js/core/utils/common'; +import { extend } from '@js/core/utils/extend'; +import { getHeight, getWidth } from '@js/core/utils/size'; +import { isDefined, isString } from '@js/core/utils/type'; +import { borderWidthStyles } from '@js/renovation/ui/resizable/utils'; +import { OverlayPositionController } from '@ts/ui/overlay/m_overlay_position_controller'; const WEIGHT_OF_SIDES = { - 'left': -1, - 'top': -1, - 'center': 0, - 'right': 1, - 'bottom': 1 + left: -1, + top: -1, + center: 0, + right: 1, + bottom: 1, }; const POPOVER_POSITION_ALIASES = { - // NOTE: public API - 'top': { my: 'bottom center', at: 'top center', collision: 'fit flip' }, - 'bottom': { my: 'top center', at: 'bottom center', collision: 'fit flip' }, - 'right': { my: 'left center', at: 'right center', collision: 'flip fit' }, - 'left': { my: 'right center', at: 'left center', collision: 'flip fit' } + // NOTE: public API + top: { my: 'bottom center', at: 'top center', collision: 'fit flip' }, + bottom: { my: 'top center', at: 'bottom center', collision: 'fit flip' }, + right: { my: 'left center', at: 'right center', collision: 'flip fit' }, + left: { my: 'right center', at: 'left center', collision: 'flip fit' }, }; const POPOVER_DEFAULT_BOUNDARY_OFFSET = { h: 10, v: 10 }; class PopoverPositionController extends OverlayPositionController { - constructor({ shading, target, $arrow, ...args }) { - super(args); + _positionSide?: any; - this._props = { - ...this._props, - shading, - target - }; + _$arrow?: dxElementWrapper; - this._$arrow = $arrow; + constructor({ + shading, + target, + $arrow, + ...args + }) { + // @ts-expect-error + super(args); - this._positionSide = undefined; + this._props = { + ...this._props, + shading, + target, + }; - this.updatePosition(this._props.position); - } - - positionWrapper() { - if(this._props.shading) { - this._$wrapper.css({ top: 0, left: 0 }); - } - } - - updateTarget(target) { - this._props.target = target; - - this.updatePosition(this._props.position); - } - - _renderBoundaryOffset() {} - - _getContainerPosition() { - const offset = pairToObject(this._position.offset || ''); - let { h: hOffset, v: vOffset } = offset; - const isVerticalSide = this._isVerticalSide(); - const isHorizontalSide = this._isHorizontalSide(); + this._$arrow = $arrow; - if(isVerticalSide || isHorizontalSide) { - const isPopoverInside = this._isPopoverInside(); - const sign = (isPopoverInside ? -1 : 1) * WEIGHT_OF_SIDES[this._positionSide]; - const arrowSize = isVerticalSide ? getHeight(this._$arrow) : getWidth(this._$arrow); - const arrowSizeCorrection = this._getContentBorderWidth(this._positionSide); - const arrowOffset = sign * (arrowSize - arrowSizeCorrection); + this._positionSide = undefined; - isVerticalSide ? vOffset += arrowOffset : hOffset += arrowOffset; - } + this.updatePosition(this._props.position); + } - return extend({}, this._position, { offset: hOffset + ' ' + vOffset }); + positionWrapper(): void { + if (this._props.shading) { + // @ts-expect-error + this._$wrapper.css({ top: 0, left: 0 }); } + } - _getContentBorderWidth(side) { - const borderWidth = this._$content.css(borderWidthStyles[side]); + updateTarget(target): void { + this._props.target = target; - return parseInt(borderWidth) || 0; - } + this.updatePosition(this._props.position); + } - _isPopoverInside() { - const my = positionUtils.setup.normalizeAlign(this._position.my); - const at = positionUtils.setup.normalizeAlign(this._position.at); + _renderBoundaryOffset(): void {} - return my.h === at.h && my.v === at.v; - } + _getContainerPosition() { + const offset = pairToObject(this._position.offset || ''); + let { h: hOffset, v: vOffset } = offset; + const isVerticalSide = this._isVerticalSide(); + const isHorizontalSide = this._isHorizontalSide(); - _isVerticalSide(side = this._positionSide) { - return side === 'top' || side === 'bottom'; - } + if (isVerticalSide || isHorizontalSide) { + const isPopoverInside = this._isPopoverInside(); + const sign = (isPopoverInside ? -1 : 1) * WEIGHT_OF_SIDES[this._positionSide]; + const arrowSize = isVerticalSide ? getHeight(this._$arrow) : getWidth(this._$arrow); + const arrowSizeCorrection = this._getContentBorderWidth(this._positionSide); + const arrowOffset = sign * (arrowSize - arrowSizeCorrection); - _isHorizontalSide(side = this._positionSide) { - return side === 'left' || side === 'right'; + isVerticalSide ? vOffset += arrowOffset : hOffset += arrowOffset; } - _getDisplaySide(position) { - const my = positionUtils.setup.normalizeAlign(position.my); - const at = positionUtils.setup.normalizeAlign(position.at); - - const weightSign = WEIGHT_OF_SIDES[my.h] === WEIGHT_OF_SIDES[at.h] && WEIGHT_OF_SIDES[my.v] === WEIGHT_OF_SIDES[at.v] ? -1 : 1; - const horizontalWeight = Math.abs(WEIGHT_OF_SIDES[my.h] - weightSign * WEIGHT_OF_SIDES[at.h]); - const verticalWeight = Math.abs(WEIGHT_OF_SIDES[my.v] - weightSign * WEIGHT_OF_SIDES[at.v]); - - return horizontalWeight > verticalWeight ? at.h : at.v; + return extend({}, this._position, { offset: `${hOffset} ${vOffset}` }); + } + + _getContentBorderWidth(side) { + // @ts-expect-error + const borderWidth = this._$content.css(borderWidthStyles[side]); + + // @ts-expect-error + // eslint-disable-next-line radix + return parseInt(borderWidth) || 0; + } + + _isPopoverInside(): boolean { + // @ts-expect-error + const my = positionUtils.setup.normalizeAlign(this._position.my); + // @ts-expect-error + const at = positionUtils.setup.normalizeAlign(this._position.at); + + return my.h === at.h && my.v === at.v; + } + + _isVerticalSide(side = this._positionSide) { + return side === 'top' || side === 'bottom'; + } + + _isHorizontalSide(side = this._positionSide) { + return side === 'left' || side === 'right'; + } + + _getDisplaySide(position) { + // @ts-expect-error + const my = positionUtils.setup.normalizeAlign(position.my); + // @ts-expect-error + const at = positionUtils.setup.normalizeAlign(position.at); + + const weightSign = WEIGHT_OF_SIDES[my.h] === WEIGHT_OF_SIDES[at.h] && WEIGHT_OF_SIDES[my.v] === WEIGHT_OF_SIDES[at.v] ? -1 : 1; + const horizontalWeight = Math.abs(WEIGHT_OF_SIDES[my.h] - weightSign * WEIGHT_OF_SIDES[at.h]); + const verticalWeight = Math.abs(WEIGHT_OF_SIDES[my.v] - weightSign * WEIGHT_OF_SIDES[at.v]); + + return horizontalWeight > verticalWeight ? at.h : at.v; + } + + _normalizePosition(positionProp) { + const defaultPositionConfig = { + of: this._props.target, + boundaryOffset: POPOVER_DEFAULT_BOUNDARY_OFFSET, + }; + + let resultPosition; + if (isDefined(positionProp)) { + resultPosition = extend(true, {}, defaultPositionConfig, this._positionToObject(positionProp)); + } else { + resultPosition = defaultPositionConfig; } - _normalizePosition(positionProp) { - const defaultPositionConfig = { - of: this._props.target, - boundaryOffset: POPOVER_DEFAULT_BOUNDARY_OFFSET - }; - - let resultPosition; - if(isDefined(positionProp)) { - resultPosition = extend(true, {}, defaultPositionConfig, this._positionToObject(positionProp)); - } else { - resultPosition = defaultPositionConfig; - } + this._positionSide = this._getDisplaySide(resultPosition); - this._positionSide = this._getDisplaySide(resultPosition); + return resultPosition; + } - return resultPosition; + _positionToObject(positionProp) { + if (isString(positionProp)) { + return extend({}, POPOVER_POSITION_ALIASES[positionProp]); } - _positionToObject(positionProp) { - if(isString(positionProp)) { - return extend({}, POPOVER_POSITION_ALIASES[positionProp]); - } - - return positionProp; - } + return positionProp; + } } export { - PopoverPositionController, - POPOVER_POSITION_ALIASES + POPOVER_POSITION_ALIASES, + PopoverPositionController, }; diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup.full.ts b/packages/devextreme/js/__internal/ui/popup/m_popup.full.ts index 89434af6c196..397a0973d174 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup.full.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup.full.ts @@ -1,21 +1,24 @@ -import '../toolbar'; -import Popup from '../popup/ui.popup'; -import registerComponent from '../../core/component_registrator'; -import { extend } from '../../core/utils/extend'; +import '@js/ui/toolbar'; + +import registerComponent from '@js/core/component_registrator'; +import { extend } from '@js/core/utils/extend'; +import Popup from '@js/ui/popup/ui.popup'; + export default class PopupFull extends Popup { - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - preventScrollEvents: false, - }); - } + _getDefaultOptions() { + return extend(super._getDefaultOptions(), { + preventScrollEvents: false, + }); + } - _getToolbarName() { - return 'dxToolbar'; - } + _getToolbarName() { + return 'dxToolbar'; + } } -PopupFull.defaultOptions = function(rule) { - Popup.defaultOptions(rule); +// @ts-expect-error +PopupFull.defaultOptions = function (rule) { + Popup.defaultOptions(rule); }; - +// @ts-expect-error registerComponent('dxPopup', PopupFull); diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup.ts b/packages/devextreme/js/__internal/ui/popup/m_popup.ts index 56ea01447d90..1df056c57e09 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup.ts @@ -1,44 +1,45 @@ -import registerComponent from '../../core/component_registrator'; -import devices from '../../core/devices'; -import { getPublicElement } from '../../core/element'; -import $ from '../../core/renderer'; -import { EmptyTemplate } from '../../core/templates/empty_template'; -import browser from '../../core/utils/browser'; -import { noop } from '../../core/utils/common'; -import { extend } from '../../core/utils/extend'; -import { camelize } from '../../core/utils/inflector'; -import { each } from '../../core/utils/iterator'; +import '@js/ui/toolbar/ui.toolbar.base'; + +import registerComponent from '@js/core/component_registrator'; +import devices from '@js/core/devices'; +import { getPublicElement } from '@js/core/element'; +import Guid from '@js/core/guid'; +import $ from '@js/core/renderer'; +import resizeObserverSingleton from '@js/core/resize_observer'; +import { EmptyTemplate } from '@js/core/templates/empty_template'; +import browser from '@js/core/utils/browser'; +import { noop } from '@js/core/utils/common'; +import { extend } from '@js/core/utils/extend'; +import { camelize } from '@js/core/utils/inflector'; +import { each } from '@js/core/utils/iterator'; +import { getBoundingRect } from '@js/core/utils/position'; import { - getVisibleHeight, - addOffsetToMaxHeight, - addOffsetToMinHeight, - getVerticalOffsets, - getOuterWidth, - getWidth, - getHeight -} from '../../core/utils/size'; -import { getBoundingRect } from '../../core/utils/position'; -import { isDefined, isObject } from '../../core/utils/type'; -import { compare as compareVersions } from '../../core/utils/version'; -import { getWindow, hasWindow } from '../../core/utils/window'; -import { triggerResizeEvent } from '../../events/visibility_change'; -import messageLocalization from '../../localization/message'; -import PopupDrag from './popup_drag'; -import Resizable from '../resizable'; -import Button from '../button'; -import Overlay from '../overlay/ui.overlay'; -import { isMaterialBased, isMaterial, isFluent } from '../themes'; -import '../../__internal/ui/toolbar/m_toolbar.base'; -import resizeObserverSingleton from '../../core/resize_observer'; -import * as zIndexPool from '../../__internal/ui/overlay/m_z_index'; -import { PopupPositionController } from './popup_position_controller'; -import { createBodyOverflowManager } from './popup_overflow_manager'; -import Guid from '../../core/guid'; + addOffsetToMaxHeight, + addOffsetToMinHeight, + getHeight, + getOuterWidth, + getVerticalOffsets, + getVisibleHeight, + getWidth, +} from '@js/core/utils/size'; +import { isDefined, isObject } from '@js/core/utils/type'; +import { compare as compareVersions } from '@js/core/utils/version'; +import { getWindow, hasWindow } from '@js/core/utils/window'; +import { triggerResizeEvent } from '@js/events/visibility_change'; +import messageLocalization from '@js/localization/message'; +import Button from '@js/ui/button'; +import Overlay from '@js/ui/overlay/ui.overlay'; +import type { ToolbarItem } from '@js/ui/popup'; +import Resizable from '@js/ui/resizable'; +import { isFluent, isMaterial, isMaterialBased } from '@js/ui/themes'; +import * as zIndexPool from '@ts/ui/overlay/m_z_index'; + +import PopupDrag from './m_popup_drag'; +import { createBodyOverflowManager } from './m_popup_overflow_manager'; +import { PopupPositionController } from './m_popup_position_controller'; const window = getWindow(); -// STYLE popup - const POPUP_CLASS = 'dx-popup'; const POPUP_WRAPPER_CLASS = 'dx-popup-wrapper'; const POPUP_FULL_SCREEN_CLASS = 'dx-popup-fullscreen'; @@ -75,1015 +76,1026 @@ const BUTTON_OUTLINED_MODE = 'outlined'; const IS_OLD_SAFARI = browser.safari && compareVersions(browser.version, [11]) < 0; const HEIGHT_STRATEGIES = { static: '', inherit: POPUP_CONTENT_INHERIT_HEIGHT_CLASS, flex: POPUP_CONTENT_FLEX_HEIGHT_CLASS }; -const getButtonPlace = name => { - - const device = devices.current(); - const platform = device.platform; - let toolbar = 'bottom'; - let location = 'before'; - - if(platform === 'ios') { - switch(name) { - case 'cancel': - toolbar = 'top'; - break; - case 'clear': - toolbar = 'top'; - location = 'after'; - break; - case 'done': - location = 'after'; - break; - } - } else if(platform === 'android') { - switch(name) { - case 'cancel': - location = 'after'; - break; - case 'done': - location = 'after'; - break; - } +const getButtonPlace = (name) => { + const device = devices.current(); + const { platform } = device; + let toolbar = 'bottom'; + let location = 'before'; + + if (platform === 'ios') { + // eslint-disable-next-line default-case + switch (name) { + case 'cancel': + toolbar = 'top'; + break; + case 'clear': + toolbar = 'top'; + location = 'after'; + break; + case 'done': + location = 'after'; + break; + } + } else if (platform === 'android') { + // eslint-disable-next-line default-case + switch (name) { + case 'cancel': + location = 'after'; + break; + case 'done': + location = 'after'; + break; } + } - return { - toolbar, - location - }; + return { + toolbar, + location, + }; }; - +// @ts-expect-error const Popup = Overlay.inherit({ - _supportedKeys: function() { - return extend(this.callBase(), { - upArrow: (e) => { this._drag?.moveUp(e); }, - downArrow: (e) => { this._drag?.moveDown(e); }, - leftArrow: (e) => { this._drag?.moveLeft(e); }, - rightArrow: (e) => { this._drag?.moveRight(e); } - }); - }, - - _getDefaultOptions: function() { - return extend(this.callBase(), { - fullScreen: false, - title: '', - showTitle: true, - titleTemplate: 'title', - onTitleRendered: null, - dragOutsideBoundary: false, - dragEnabled: false, - dragAndResizeArea: undefined, - enableBodyScroll: true, - outsideDragFactor: 0, - onResizeStart: null, - onResize: null, - onResizeEnd: null, - resizeEnabled: false, - toolbarItems: [], - showCloseButton: false, - bottomTemplate: 'bottom', - useDefaultToolbarButtons: false, - useFlatToolbarButtons: false, - autoResizeEnabled: true - }); - }, - - _defaultOptionsRules: function() { - return this.callBase().concat([ - { - device: { platform: 'ios' }, - options: { - animation: this._iosAnimation - } - }, - { - device: { platform: 'android' }, - options: { - animation: this._androidAnimation - } - }, - { - device: { platform: 'generic' }, - options: { - showCloseButton: true - } - }, - { - device: function(device) { - return devices.real().deviceType === 'desktop' && device.platform === 'generic'; - }, - options: { - dragEnabled: true - } - }, - { - device: function() { - return devices.real().deviceType === 'desktop' && !devices.isSimulator(); - }, - options: { - focusStateEnabled: true - } - }, - { - device: function() { - return isMaterialBased(); - }, - options: { - useFlatToolbarButtons: true, - } - }, - { - device: function() { - return isMaterial(); - }, - options: { - useDefaultToolbarButtons: true, - showCloseButton: false - } - }, - ]); - }, - - _iosAnimation: { - show: { - type: 'slide', - duration: 400, - from: { - position: { - my: 'top', - at: 'bottom' - } - }, - to: { - position: { - my: 'center', - at: 'center' - } - } + _supportedKeys() { + return extend(this.callBase(), { + upArrow: (e) => { this._drag?.moveUp(e); }, + downArrow: (e) => { this._drag?.moveDown(e); }, + leftArrow: (e) => { this._drag?.moveLeft(e); }, + rightArrow: (e) => { this._drag?.moveRight(e); }, + }); + }, + + _getDefaultOptions() { + return extend(this.callBase(), { + fullScreen: false, + title: '', + showTitle: true, + titleTemplate: 'title', + onTitleRendered: null, + dragOutsideBoundary: false, + dragEnabled: false, + dragAndResizeArea: undefined, + enableBodyScroll: true, + outsideDragFactor: 0, + onResizeStart: null, + onResize: null, + onResizeEnd: null, + resizeEnabled: false, + toolbarItems: [], + showCloseButton: false, + bottomTemplate: 'bottom', + useDefaultToolbarButtons: false, + useFlatToolbarButtons: false, + autoResizeEnabled: true, + }); + }, + + _defaultOptionsRules() { + return this.callBase().concat([ + { + device: { platform: 'ios' }, + options: { + animation: this._iosAnimation, }, - hide: { - type: 'slide', - duration: 400, - from: { - opacity: 1, - position: { - my: 'center', - at: 'center' - } - }, - to: { - opacity: 1, - position: { - my: 'top', - at: 'bottom' - } - } - } - }, - - _androidAnimation: function() { - const fullScreenConfig = { - show: { type: 'slide', duration: 300, from: { top: '30%', opacity: 0 }, to: { top: 0, opacity: 1 } }, - hide: { type: 'slide', duration: 300, from: { top: 0, opacity: 1 }, to: { top: '30%', opacity: 0 } } - }; - const defaultConfig = { - show: { type: 'fade', duration: 400, from: 0, to: 1 }, - hide: { type: 'fade', duration: 400, from: 1, to: 0 } - }; + }, + { + device: { platform: 'android' }, + options: { + animation: this._androidAnimation, + }, + }, + { + device: { platform: 'generic' }, + options: { + showCloseButton: true, + }, + }, + { + device(device) { + return devices.real().deviceType === 'desktop' && device.platform === 'generic'; + }, + options: { + dragEnabled: true, + }, + }, + { + device() { + return devices.real().deviceType === 'desktop' && !devices.isSimulator(); + }, + options: { + focusStateEnabled: true, + }, + }, + { + device() { + // @ts-expect-error + return isMaterialBased(); + }, + options: { + useFlatToolbarButtons: true, + }, + }, + { + device() { + // @ts-expect-error + return isMaterial(); + }, + options: { + useDefaultToolbarButtons: true, + showCloseButton: false, + }, + }, + ]); + }, + + _iosAnimation: { + show: { + type: 'slide', + duration: 400, + from: { + position: { + my: 'top', + at: 'bottom', + }, + }, + to: { + position: { + my: 'center', + at: 'center', + }, + }, + }, + hide: { + type: 'slide', + duration: 400, + from: { + opacity: 1, + position: { + my: 'center', + at: 'center', + }, + }, + to: { + opacity: 1, + position: { + my: 'top', + at: 'bottom', + }, + }, + }, + }, + + _androidAnimation() { + const fullScreenConfig = { + show: { + type: 'slide', duration: 300, from: { top: '30%', opacity: 0 }, to: { top: 0, opacity: 1 }, + }, + hide: { + type: 'slide', duration: 300, from: { top: 0, opacity: 1 }, to: { top: '30%', opacity: 0 }, + }, + }; + const defaultConfig = { + show: { + type: 'fade', duration: 400, from: 0, to: 1, + }, + hide: { + type: 'fade', duration: 400, from: 1, to: 0, + }, + }; - return this.option('fullScreen') ? fullScreenConfig : defaultConfig; - }, + return this.option('fullScreen') ? fullScreenConfig : defaultConfig; + }, - _init: function() { - const popupWrapperClassExternal = this.option('_wrapperClassExternal'); - const popupWrapperClasses = popupWrapperClassExternal - ? `${POPUP_WRAPPER_CLASS} ${popupWrapperClassExternal}` - : POPUP_WRAPPER_CLASS; + _init() { + const popupWrapperClassExternal = this.option('_wrapperClassExternal'); + const popupWrapperClasses = popupWrapperClassExternal + ? `${POPUP_WRAPPER_CLASS} ${popupWrapperClassExternal}` + : POPUP_WRAPPER_CLASS; - this.callBase(); + this.callBase(); - this._createBodyOverflowManager(); - this._updateResizeCallbackSkipCondition(); + this._createBodyOverflowManager(); + this._updateResizeCallbackSkipCondition(); - this.$element().addClass(POPUP_CLASS); - this.$wrapper().addClass(popupWrapperClasses); - this._$popupContent = this._$content - .wrapInner($('
').addClass(POPUP_CONTENT_CLASS)) - .children().eq(0); + this.$element().addClass(POPUP_CLASS); + this.$wrapper().addClass(popupWrapperClasses); + this._$popupContent = this._$content + .wrapInner($('
').addClass(POPUP_CONTENT_CLASS)) + .children().eq(0); - this._toggleContentScrollClass(); + this._toggleContentScrollClass(); - this.$overlayContent().attr('role', 'dialog'); - }, + this.$overlayContent().attr('role', 'dialog'); + }, - _render: function() { - const isFullscreen = this.option('fullScreen'); + _render() { + const isFullscreen = this.option('fullScreen'); - this._toggleFullScreenClass(isFullscreen); - this.callBase(); - }, + this._toggleFullScreenClass(isFullscreen); + this.callBase(); + }, - _createBodyOverflowManager: function() { - this._bodyOverflowManager = createBodyOverflowManager(); - }, + _createBodyOverflowManager() { + this._bodyOverflowManager = createBodyOverflowManager(); + }, - _toggleFullScreenClass: function(value) { - this.$overlayContent() - .toggleClass(POPUP_FULL_SCREEN_CLASS, value) - .toggleClass(POPUP_NORMAL_CLASS, !value); - }, + _toggleFullScreenClass(value) { + this.$overlayContent() + .toggleClass(POPUP_FULL_SCREEN_CLASS, value) + .toggleClass(POPUP_NORMAL_CLASS, !value); + }, - _initTemplates: function() { - this.callBase(); - this._templateManager.addDefaultTemplates({ - title: new EmptyTemplate(), - bottom: new EmptyTemplate() - }); - }, + _initTemplates() { + this.callBase(); + this._templateManager.addDefaultTemplates({ + title: new EmptyTemplate(), + bottom: new EmptyTemplate(), + }); + }, - _getActionsList: function() { - return this.callBase().concat(['onResizeStart', 'onResize', 'onResizeEnd']); - }, + _getActionsList() { + return this.callBase().concat(['onResizeStart', 'onResize', 'onResizeEnd']); + }, - _contentResizeHandler: function(entry) { - if(!this._shouldSkipContentResize(entry)) { - this._renderGeometry({ shouldOnlyReposition: true }); - } - }, + _contentResizeHandler(entry) { + if (!this._shouldSkipContentResize(entry)) { + this._renderGeometry({ shouldOnlyReposition: true }); + } + }, - _doesShowAnimationChangeDimensions: function() { - const animation = this.option('animation'); + _doesShowAnimationChangeDimensions() { + const animation = this.option('animation'); - return ['to', 'from'].some(prop => { - const config = animation?.show?.[prop]; - return isObject(config) && ('width' in config || 'height' in config); - }); - }, + return ['to', 'from'].some((prop) => { + const config = animation?.show?.[prop]; + return isObject(config) && ('width' in config || 'height' in config); + }); + }, - _updateResizeCallbackSkipCondition() { - const doesShowAnimationChangeDimensions = this._doesShowAnimationChangeDimensions(); + _updateResizeCallbackSkipCondition() { + const doesShowAnimationChangeDimensions = this._doesShowAnimationChangeDimensions(); - this._shouldSkipContentResize = (entry) => { - return doesShowAnimationChangeDimensions && this._showAnimationProcessing + this._shouldSkipContentResize = (entry) => doesShowAnimationChangeDimensions && this._showAnimationProcessing || this._areContentDimensionsRendered(entry); - }; - }, + }, - _observeContentResize: function(shouldObserve) { - if(!this.option('useResizeObserver')) { - return; - } + _observeContentResize(shouldObserve) { + if (!this.option('useResizeObserver')) { + return; + } - const contentElement = this._$content.get(0); - if(shouldObserve) { - resizeObserverSingleton.observe(contentElement, (entry) => { this._contentResizeHandler(entry); }); - } else { - resizeObserverSingleton.unobserve(contentElement); - } - }, + const contentElement = this._$content.get(0); + if (shouldObserve) { + resizeObserverSingleton.observe(contentElement, (entry) => { this._contentResizeHandler(entry); }); + } else { + resizeObserverSingleton.unobserve(contentElement); + } + }, - _areContentDimensionsRendered: function(entry) { - const contentBox = entry.contentBoxSize?.[0]; - if(contentBox) { - return parseInt(contentBox.inlineSize, 10) === this._renderedDimensions?.width + _areContentDimensionsRendered(entry) { + const contentBox = entry.contentBoxSize?.[0]; + if (contentBox) { + return parseInt(contentBox.inlineSize, 10) === this._renderedDimensions?.width && parseInt(contentBox.blockSize, 10) === this._renderedDimensions?.height; - } + } - const contentRect = entry.contentRect; - return parseInt(contentRect.width, 10) === this._renderedDimensions?.width + const { contentRect } = entry; + return parseInt(contentRect.width, 10) === this._renderedDimensions?.width && parseInt(contentRect.height, 10) === this._renderedDimensions?.height; - }, - - _renderContent() { - this.callBase(); - // NOTE: This observe should not be called before async showing is called. See T1130045. - this._observeContentResize(true); - }, - - _renderContentImpl: function() { - this._renderTitle(); - this.callBase(); - this._renderResize(); - this._renderBottom(); - }, - - _renderTitle: function() { - const items = this._getToolbarItems('top'); - const { title, showTitle } = this.option(); - - if(showTitle && !!title) { - items.unshift({ - location: devices.current().ios ? 'center' : 'before', - text: title, - }); - } - - if(showTitle || items.length > 0) { - this._$title && this._$title.remove(); - const $title = $('
').addClass(POPUP_TITLE_CLASS).insertBefore(this.$content()); - this._$title = this._renderTemplateByType('titleTemplate', items, $title).addClass(POPUP_TITLE_CLASS); - this._renderDrag(); - this._executeTitleRenderAction(this._$title); - this._$title.toggleClass(POPUP_HAS_CLOSE_BUTTON_CLASS, this._hasCloseButton()); - } else if(this._$title) { - this._$title.detach(); - } - - this._toggleAriaLabel(); - }, - - _toggleAriaLabel() { - const { title, showTitle } = this.option(); - const shouldSetAriaLabel = showTitle && !!title; - const titleId = shouldSetAriaLabel ? new Guid() : null; - - this._$title?.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0).attr('id', titleId); - this.$overlayContent().attr('aria-labelledby', titleId); - }, - - _renderTemplateByType: function(optionName, data, $container, additionalToolbarOptions) { - const { rtlEnabled, useDefaultToolbarButtons, useFlatToolbarButtons, disabled } = this.option(); - const template = this._getTemplateByOption(optionName); - const toolbarTemplate = template instanceof EmptyTemplate; - - if(toolbarTemplate) { - const integrationOptions = extend({}, this.option('integrationOptions'), { skipTemplates: ['content', 'title'] }); - const toolbarOptions = extend(additionalToolbarOptions, { - items: data, - rtlEnabled, - useDefaultButtons: useDefaultToolbarButtons, - useFlatButtons: useFlatToolbarButtons, - disabled, - integrationOptions - }); - - this._getTemplate('dx-polymorph-widget').render({ - container: $container, - model: { - widget: this._getToolbarName(), - options: toolbarOptions - } - }); - const $toolbar = $container.children('div'); - $container.replaceWith($toolbar); - return $toolbar; - } else { - const $result = $(template.render({ container: getPublicElement($container) })); - if($result.hasClass(TEMPLATE_WRAPPER_CLASS)) { - $container.replaceWith($result); - $container = $result; - } - return $container; - } - }, - - _getToolbarName: function() { - return 'dxToolbarBase'; - }, - - _renderVisibilityAnimate: function(visible) { - return this.callBase(visible); - }, - - _hide() { - this._observeContentResize(false); - - return this.callBase(); - }, - - _executeTitleRenderAction: function($titleElement) { - this._getTitleRenderAction()({ - titleElement: getPublicElement($titleElement) - }); - }, - - _getTitleRenderAction: function() { - return this._titleRenderAction || this._createTitleRenderAction(); - }, - - _createTitleRenderAction: function() { - return (this._titleRenderAction = this._createActionByOption('onTitleRendered', { - element: this.element(), - excludeValidators: ['disabled', 'readOnly'] - })); - }, - - _getCloseButton: function() { - return { - toolbar: 'top', - location: 'after', - template: this._getCloseButtonRenderer() - }; - }, - - _getCloseButtonRenderer: function() { - return (_, __, container) => { - const $button = $('
').addClass(POPUP_TITLE_CLOSEBUTTON_CLASS); - this._createComponent($button, Button, { - icon: 'close', - onClick: this._createToolbarItemAction(undefined), - stylingMode: 'text', - integrationOptions: {} - }); - $(container).append($button); - }; - }, - - _getToolbarItems: function(toolbar) { - - const toolbarItems = this.option('toolbarItems'); + }, + + _renderContent() { + this.callBase(); + // NOTE: This observe should not be called before async showing is called. See T1130045. + this._observeContentResize(true); + }, + + _renderContentImpl() { + this._renderTitle(); + this.callBase(); + this._renderResize(); + this._renderBottom(); + }, + + _renderTitle() { + const items = this._getToolbarItems('top'); + const { title, showTitle } = this.option(); + + if (showTitle && !!title) { + items.unshift({ + location: devices.current().ios ? 'center' : 'before', + text: title, + }); + } - const toolbarsItems = []; + if (showTitle || items.length > 0) { + this._$title && this._$title.remove(); + const $title = $('
').addClass(POPUP_TITLE_CLASS).insertBefore(this.$content()); + this._$title = this._renderTemplateByType('titleTemplate', items, $title).addClass(POPUP_TITLE_CLASS); + this._renderDrag(); + this._executeTitleRenderAction(this._$title); + this._$title.toggleClass(POPUP_HAS_CLOSE_BUTTON_CLASS, this._hasCloseButton()); + } else if (this._$title) { + this._$title.detach(); + } - this._toolbarItemClasses = []; + this._toggleAriaLabel(); + }, + + _toggleAriaLabel() { + const { title, showTitle } = this.option(); + const shouldSetAriaLabel = showTitle && !!title; + const titleId = shouldSetAriaLabel ? new Guid() : null; + + this._$title?.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0).attr('id', titleId); + this.$overlayContent().attr('aria-labelledby', titleId); + }, + + _renderTemplateByType(optionName, data, $container, additionalToolbarOptions) { + const { + rtlEnabled, useDefaultToolbarButtons, useFlatToolbarButtons, disabled, + } = this.option(); + const template = this._getTemplateByOption(optionName); + const toolbarTemplate = template instanceof EmptyTemplate; + + if (toolbarTemplate) { + const integrationOptions = extend({}, this.option('integrationOptions'), { skipTemplates: ['content', 'title'] }); + const toolbarOptions = extend(additionalToolbarOptions, { + items: data, + rtlEnabled, + useDefaultButtons: useDefaultToolbarButtons, + useFlatButtons: useFlatToolbarButtons, + disabled, + integrationOptions, + }); + + this._getTemplate('dx-polymorph-widget').render({ + container: $container, + model: { + widget: this._getToolbarName(), + options: toolbarOptions, + }, + }); + const $toolbar = $container.children('div'); + $container.replaceWith($toolbar); + return $toolbar; + } + const $result = $(template.render({ container: getPublicElement($container) })); + if ($result.hasClass(TEMPLATE_WRAPPER_CLASS)) { + $container.replaceWith($result); + $container = $result; + } + return $container; + }, + + _getToolbarName() { + return 'dxToolbarBase'; + }, + + _renderVisibilityAnimate(visible) { + return this.callBase(visible); + }, + + _hide() { + this._observeContentResize(false); + + return this.callBase(); + }, + + _executeTitleRenderAction($titleElement) { + this._getTitleRenderAction()({ + titleElement: getPublicElement($titleElement), + }); + }, + + _getTitleRenderAction() { + return this._titleRenderAction || this._createTitleRenderAction(); + }, + + _createTitleRenderAction() { + // eslint-disable-next-line no-return-assign + return (this._titleRenderAction = this._createActionByOption('onTitleRendered', { + element: this.element(), + excludeValidators: ['disabled', 'readOnly'], + })); + }, + + _getCloseButton(): ToolbarItem { + return { + toolbar: 'top', + location: 'after', + template: this._getCloseButtonRenderer(), + }; + }, + + _getCloseButtonRenderer() { + return (_, __, container) => { + const $button = $('
').addClass(POPUP_TITLE_CLOSEBUTTON_CLASS); + this._createComponent($button, Button, { + icon: 'close', + onClick: this._createToolbarItemAction(undefined), + stylingMode: 'text', + integrationOptions: {}, + }); + $(container).append($button); + }; + }, - const currentPlatform = devices.current().platform; - let index = 0; + _getToolbarItems(toolbar) { + const toolbarItems = this.option('toolbarItems'); - each(toolbarItems, (_, data) => { - const isShortcut = isDefined(data.shortcut); - const item = isShortcut ? getButtonPlace(data.shortcut) : data; + const toolbarsItems: ToolbarItem[] = []; - if(isShortcut && currentPlatform === 'ios' && index < 2) { - item.toolbar = 'top'; - index++; - } + this._toolbarItemClasses = []; - item.toolbar = data.toolbar || item.toolbar || 'top'; + const currentPlatform = devices.current().platform; + let index = 0; - if(item && item.toolbar === toolbar) { - if(isShortcut) { - extend(item, { location: data.location }, this._getToolbarItemByAlias(data)); - } + each(toolbarItems, (_, data) => { + const isShortcut = isDefined(data.shortcut); + const item = isShortcut ? getButtonPlace(data.shortcut) : data; - const isLTROrder = currentPlatform === 'generic'; + if (isShortcut && currentPlatform === 'ios' && index < 2) { + item.toolbar = 'top'; + index++; + } - if((data.shortcut === 'done' && isLTROrder) || (data.shortcut === 'cancel' && !isLTROrder)) { - toolbarsItems.unshift(item); - } else { - toolbarsItems.push(item); - } - } - }); + item.toolbar = data.toolbar || item.toolbar || 'top'; - if(toolbar === 'top' && this._hasCloseButton()) { - toolbarsItems.push(this._getCloseButton()); + if (item && item.toolbar === toolbar) { + if (isShortcut) { + extend(item, { location: data.location }, this._getToolbarItemByAlias(data)); } - return toolbarsItems; - }, - - _hasCloseButton() { - return this.option('showCloseButton') && this.option('showTitle'); - }, - - _getLocalizationKey(itemType) { - return itemType.toLowerCase() === 'done' ? 'OK' : camelize(itemType, true); - }, + const isLTROrder = currentPlatform === 'generic'; - _getToolbarButtonStylingMode: function(shortcut) { - if(isFluent()) { - return shortcut === 'done' ? BUTTON_CONTAINED_MODE : BUTTON_OUTLINED_MODE; + if ((data.shortcut === 'done' && isLTROrder) || (data.shortcut === 'cancel' && !isLTROrder)) { + toolbarsItems.unshift(item); + } else { + toolbarsItems.push(item); } + } + }); - return this.option('useFlatToolbarButtons') ? BUTTON_TEXT_MODE : BUTTON_CONTAINED_MODE; - }, + if (toolbar === 'top' && this._hasCloseButton()) { + toolbarsItems.push(this._getCloseButton()); + } - _getToolbarButtonType: function(shortcut) { - if((isFluent() && shortcut === 'done') || this.option('useDefaultToolbarButtons')) { - return BUTTON_DEFAULT_TYPE; - } + return toolbarsItems; + }, - return BUTTON_NORMAL_TYPE; - }, + _hasCloseButton() { + return this.option('showCloseButton') && this.option('showTitle'); + }, - _getToolbarItemByAlias: function(data) { - const that = this; - const itemType = data.shortcut; + _getLocalizationKey(itemType) { + return itemType.toLowerCase() === 'done' ? 'OK' : camelize(itemType, true); + }, - if(!ALLOWED_TOOLBAR_ITEM_ALIASES.includes(itemType)) { - return false; - } + _getToolbarButtonStylingMode(shortcut) { + // @ts-expect-error + if (isFluent()) { + return shortcut === 'done' ? BUTTON_CONTAINED_MODE : BUTTON_OUTLINED_MODE; + } - const itemConfig = extend({ - text: messageLocalization.format(this._getLocalizationKey(itemType)), - onClick: this._createToolbarItemAction(data.onClick), - integrationOptions: {}, - type: this._getToolbarButtonType(itemType), - stylingMode: this._getToolbarButtonStylingMode(itemType), - }, data.options || {}); + return this.option('useFlatToolbarButtons') ? BUTTON_TEXT_MODE : BUTTON_CONTAINED_MODE; + }, - const itemClass = POPUP_CLASS + '-' + itemType; + _getToolbarButtonType(shortcut) { + // @ts-expect-error + if ((isFluent() && shortcut === 'done') || this.option('useDefaultToolbarButtons')) { + return BUTTON_DEFAULT_TYPE; + } - this._toolbarItemClasses.push(itemClass); + return BUTTON_NORMAL_TYPE; + }, - return { - template: function(_, __, container) { - const $toolbarItem = $('
').addClass(itemClass).appendTo(container); - that._createComponent($toolbarItem, Button, itemConfig); - } - }; - }, + _getToolbarItemByAlias(data) { + const that = this; + const itemType = data.shortcut; - _createToolbarItemAction: function(clickAction) { - return this._createAction(clickAction, { - afterExecute: function(e) { - e.component.hide(); - } - }); - }, + if (!ALLOWED_TOOLBAR_ITEM_ALIASES.includes(itemType)) { + return false; + } - _renderBottom: function() { - const items = this._getToolbarItems('bottom'); + const itemConfig = extend({ + text: messageLocalization.format(this._getLocalizationKey(itemType)), + onClick: this._createToolbarItemAction(data.onClick), + integrationOptions: {}, + type: this._getToolbarButtonType(itemType), + stylingMode: this._getToolbarButtonStylingMode(itemType), + }, data.options || {}); - if(items.length) { - this._$bottom && this._$bottom.remove(); - const $bottom = $('
').addClass(POPUP_BOTTOM_CLASS).insertAfter(this.$content()); - this._$bottom = this._renderTemplateByType('bottomTemplate', items, $bottom, { compactMode: true }).addClass(POPUP_BOTTOM_CLASS); - this._toggleClasses(); - } else { - this._$bottom && this._$bottom.detach(); - } - }, + const itemClass = `${POPUP_CLASS}-${itemType}`; - _toggleDisabledState: function(value) { - this.callBase(...arguments); + this._toolbarItemClasses.push(itemClass); - this.$content().toggleClass(DISABLED_STATE_CLASS, Boolean(value)); - }, + return { + template(_, __, container) { + const $toolbarItem = $('
').addClass(itemClass).appendTo(container); + that._createComponent($toolbarItem, Button, itemConfig); + }, + }; + }, + + _createToolbarItemAction(clickAction) { + return this._createAction(clickAction, { + afterExecute(e) { + e.component.hide(); + }, + }); + }, + + _renderBottom() { + const items = this._getToolbarItems('bottom'); + + if (items.length) { + this._$bottom && this._$bottom.remove(); + const $bottom = $('
').addClass(POPUP_BOTTOM_CLASS).insertAfter(this.$content()); + this._$bottom = this._renderTemplateByType('bottomTemplate', items, $bottom, { compactMode: true }).addClass(POPUP_BOTTOM_CLASS); + this._toggleClasses(); + } else { + this._$bottom && this._$bottom.detach(); + } + }, - _toggleClasses: function() { - const aliases = ALLOWED_TOOLBAR_ITEM_ALIASES; + _toggleDisabledState(value) { + this.callBase(...arguments); - each(aliases, (_, alias) => { - const className = POPUP_CLASS + '-' + alias; + this.$content().toggleClass(DISABLED_STATE_CLASS, Boolean(value)); + }, - if(this._toolbarItemClasses.includes(className)) { - this.$wrapper().addClass(className + '-visible'); - this._$bottom.addClass(className); - } else { - this.$wrapper().removeClass(className + '-visible'); - this._$bottom.removeClass(className); - } - }); - }, + _toggleClasses() { + const aliases = ALLOWED_TOOLBAR_ITEM_ALIASES; - _toggleFocusClass(isFocused, $element) { - this.callBase(isFocused, $element); + each(aliases, (_, alias) => { + const className = `${POPUP_CLASS}-${alias}`; - if(isFocused && !zIndexPool.isLastZIndexInStack(this._zIndex)) { - const zIndex = zIndexPool.create(this._zIndexInitValue()); - zIndexPool.remove(this._zIndex); - this._zIndex = zIndex; + if (this._toolbarItemClasses.includes(className)) { + this.$wrapper().addClass(`${className}-visible`); + this._$bottom.addClass(className); + } else { + this.$wrapper().removeClass(`${className}-visible`); + this._$bottom.removeClass(className); + } + }); + }, - this._$wrapper.css('zIndex', zIndex); - this._$content.css('zIndex', zIndex); - } - }, + _toggleFocusClass(isFocused, $element) { + this.callBase(isFocused, $element); - _toggleContentScrollClass() { - const isNativeScrollingEnabled = !this.option('preventScrollEvents'); + if (isFocused && !zIndexPool.isLastZIndexInStack(this._zIndex)) { + const zIndex = zIndexPool.create(this._zIndexInitValue()); + zIndexPool.remove(this._zIndex); + this._zIndex = zIndex; - this.$content().toggleClass(POPUP_CONTENT_SCROLLABLE_CLASS, isNativeScrollingEnabled); - }, + this._$wrapper.css('zIndex', zIndex); + this._$content.css('zIndex', zIndex); + } + }, + + _toggleContentScrollClass() { + const isNativeScrollingEnabled = !this.option('preventScrollEvents'); + + this.$content().toggleClass(POPUP_CONTENT_SCROLLABLE_CLASS, isNativeScrollingEnabled); + }, + + _getPositionControllerConfig() { + const { + fullScreen, + forceApplyBindings, + dragOutsideBoundary, + dragAndResizeArea, + outsideDragFactor, + } = this.option(); + + return extend({}, this.callBase(), { + fullScreen, + forceApplyBindings, + dragOutsideBoundary, + dragAndResizeArea, + outsideDragFactor, + }); + }, + + _initPositionController() { + this._positionController = new PopupPositionController( + this._getPositionControllerConfig(), + ); + }, + + _getDragTarget() { + return this.topToolbar(); + }, + + _renderGeometry(options) { + const { visible, useResizeObserver } = this.option(); + + if (visible && hasWindow()) { + const isAnimated = this._showAnimationProcessing; + const shouldRepeatAnimation = isAnimated && !options?.forceStopAnimation && useResizeObserver; + this._isAnimationPaused = shouldRepeatAnimation || undefined; + + this._stopAnimation(); + if (options?.shouldOnlyReposition) { + this._renderPosition(false); + } else { + this._renderGeometryImpl(options?.isDimensionChange); + } + + if (shouldRepeatAnimation) { + this._animateShowing(); + this._isAnimationPaused = undefined; + } + } + }, - _getPositionControllerConfig() { - const { - fullScreen, - forceApplyBindings, - dragOutsideBoundary, - dragAndResizeArea, - outsideDragFactor - } = this.option(); - - return extend({}, this.callBase(), { - fullScreen, - forceApplyBindings, - dragOutsideBoundary, - dragAndResizeArea, - outsideDragFactor - }); - }, + _cacheDimensions() { + if (!this.option('useResizeObserver')) { + return; + } - _initPositionController() { - this._positionController = new PopupPositionController( - this._getPositionControllerConfig() - ); - }, + this._renderedDimensions = { + width: parseInt(getWidth(this._$content), 10), + height: parseInt(getHeight(this._$content), 10), + }; + }, - _getDragTarget: function() { - return this.topToolbar(); - }, + _renderGeometryImpl(isDimensionChange = false) { + if (!isDimensionChange) { // NOTE: to save content scroll position T1113123 + // NOTE: for correct new position calculation + this._resetContentHeight(); + } + this.callBase(); + this._cacheDimensions(); + this._setContentHeight(); + }, + + _resetContentHeight() { + const height = this._getOptionValue('height'); + + if (height === 'auto') { + this.$content().css({ + height: 'auto', + maxHeight: 'none', + }); + } + }, - _renderGeometry: function(options) { - const { visible, useResizeObserver } = this.option(); - - if(visible && hasWindow()) { - const isAnimated = this._showAnimationProcessing; - const shouldRepeatAnimation = isAnimated && !options?.forceStopAnimation && useResizeObserver; - this._isAnimationPaused = shouldRepeatAnimation || undefined; - - this._stopAnimation(); - if(options?.shouldOnlyReposition) { - this._renderPosition(false); - } else { - this._renderGeometryImpl(options?.isDimensionChange); - } - - if(shouldRepeatAnimation) { - this._animateShowing(); - this._isAnimationPaused = undefined; - } - } - }, + _renderDrag() { + const $dragTarget = this._getDragTarget(); + const dragEnabled = this.option('dragEnabled'); - _cacheDimensions: function() { - if(!this.option('useResizeObserver')) { - return; - } + if (!$dragTarget) { + return; + } - this._renderedDimensions = { - width: parseInt(getWidth(this._$content), 10), - height: parseInt(getHeight(this._$content), 10) - }; - }, + const config = { + dragEnabled, + handle: $dragTarget.get(0), + draggableElement: this._$content.get(0), + positionController: this._positionController, + }; - _renderGeometryImpl: function(isDimensionChange = false) { - if(!isDimensionChange) { // NOTE: to save content scroll position T1113123 - // NOTE: for correct new position calculation - this._resetContentHeight(); - } - this.callBase(); - this._cacheDimensions(); - this._setContentHeight(); - }, + if (this._drag) { + this._drag.init(config); + } else { + this._drag = new PopupDrag(config); + } - _resetContentHeight: function() { - const height = this._getOptionValue('height'); + this.$overlayContent().toggleClass(POPUP_DRAGGABLE_CLASS, dragEnabled); + }, - if(height === 'auto') { - this.$content().css({ - height: 'auto', - maxHeight: 'none' - }); - } - }, + _renderResize() { + this._resizable = this._createComponent(this._$content, Resizable, { + handles: this.option('resizeEnabled') ? 'all' : 'none', + onResizeEnd: (e) => { + this._resizeEndHandler(e); + this._observeContentResize(true); + }, + onResize: (e) => { + this._setContentHeight(); + this._actions.onResize(e); + }, + onResizeStart: (e) => { + this._observeContentResize(false); + this._actions.onResizeStart(e); + }, + minHeight: 100, + minWidth: 100, + area: this._positionController.$dragResizeContainer, + keepAspectRatio: false, + }); + }, + + _resizeEndHandler(e) { + const width = this._resizable.option('width'); + const height = this._resizable.option('height'); + + width && this._setOptionWithoutOptionChange('width', width); + height && this._setOptionWithoutOptionChange('height', height); + this._cacheDimensions(); + + this._positionController.resizeHandled(); + this._positionController.detectVisualPositionChange(e.event); + + this._actions.onResizeEnd(e); + }, + + _setContentHeight() { + (this.option('forceApplyBindings') || noop)(); + + const overlayContent = this.$overlayContent().get(0); + const currentHeightStrategyClass = this._chooseHeightStrategy(overlayContent); + + this.$content().css(this._getHeightCssStyles(currentHeightStrategyClass, overlayContent)); + this._setHeightClasses(this.$overlayContent(), currentHeightStrategyClass); + }, + + _heightStrategyChangeOffset(currentHeightStrategyClass, popupVerticalPaddings) { + return currentHeightStrategyClass === HEIGHT_STRATEGIES.flex ? -popupVerticalPaddings : 0; + }, + + _chooseHeightStrategy(overlayContent) { + const isAutoWidth = overlayContent.style.width === 'auto' || overlayContent.style.width === ''; + let currentHeightStrategyClass = HEIGHT_STRATEGIES.static; + + if (this._isAutoHeight() && this.option('autoResizeEnabled')) { + if (isAutoWidth || IS_OLD_SAFARI) { + currentHeightStrategyClass = HEIGHT_STRATEGIES.inherit; + } else { + currentHeightStrategyClass = HEIGHT_STRATEGIES.flex; + } + } - _renderDrag: function() { - const $dragTarget = this._getDragTarget(); - const dragEnabled = this.option('dragEnabled'); + return currentHeightStrategyClass; + }, - if(!$dragTarget) { - return; - } + _getHeightCssStyles(currentHeightStrategyClass, overlayContent) { + let cssStyles = {}; + const contentMaxHeight = this._getOptionValue('maxHeight', overlayContent); + const contentMinHeight = this._getOptionValue('minHeight', overlayContent); + const popupHeightParts = this._splitPopupHeight(); + const toolbarsAndVerticalOffsetsHeight = popupHeightParts.header + + popupHeightParts.footer + + popupHeightParts.contentVerticalOffsets + + popupHeightParts.popupVerticalOffsets + + this._heightStrategyChangeOffset(currentHeightStrategyClass, popupHeightParts.popupVerticalPaddings); - const config = { - dragEnabled, - handle: $dragTarget.get(0), - draggableElement: this._$content.get(0), - positionController: this._positionController + if (currentHeightStrategyClass === HEIGHT_STRATEGIES.static) { + if (!this._isAutoHeight() || contentMaxHeight || contentMinHeight) { + const overlayHeight = this.option('fullScreen') + ? Math.min(getBoundingRect(overlayContent).height, getWindow().innerHeight) + : getBoundingRect(overlayContent).height; + const contentHeight = overlayHeight - toolbarsAndVerticalOffsetsHeight; + cssStyles = { + height: Math.max(0, contentHeight), + minHeight: 'auto', + maxHeight: 'auto', }; + } + } else { + const container = $(this._positionController.$visualContainer).get(0); + const maxHeightValue = addOffsetToMaxHeight(contentMaxHeight, -toolbarsAndVerticalOffsetsHeight, container); + const minHeightValue = addOffsetToMinHeight(contentMinHeight, -toolbarsAndVerticalOffsetsHeight, container); + + cssStyles = { + height: 'auto', + minHeight: minHeightValue, + maxHeight: maxHeightValue, + }; + } - if(this._drag) { - this._drag.init(config); - } else { - this._drag = new PopupDrag(config); - } - - this.$overlayContent().toggleClass(POPUP_DRAGGABLE_CLASS, dragEnabled); - }, - - _renderResize: function() { - this._resizable = this._createComponent(this._$content, Resizable, { - handles: this.option('resizeEnabled') ? 'all' : 'none', - onResizeEnd: (e) => { - this._resizeEndHandler(e); - this._observeContentResize(true); - }, - onResize: (e) => { - this._setContentHeight(); - this._actions.onResize(e); - }, - onResizeStart: (e) => { - this._observeContentResize(false); - this._actions.onResizeStart(e); - }, - minHeight: 100, - minWidth: 100, - area: this._positionController.$dragResizeContainer, - keepAspectRatio: false - }); - }, - - _resizeEndHandler: function(e) { - const width = this._resizable.option('width'); - const height = this._resizable.option('height'); - - width && this._setOptionWithoutOptionChange('width', width); - height && this._setOptionWithoutOptionChange('height', height); - this._cacheDimensions(); - - this._positionController.resizeHandled(); - this._positionController.detectVisualPositionChange(e.event); + return cssStyles; + }, - this._actions.onResizeEnd(e); - }, + _setHeightClasses($container, currentClass) { + let excessClasses = ''; - _setContentHeight: function() { - (this.option('forceApplyBindings') || noop)(); + // eslint-disable-next-line no-restricted-syntax + for (const name in HEIGHT_STRATEGIES) { + if (HEIGHT_STRATEGIES[name] !== currentClass) { + excessClasses += ` ${HEIGHT_STRATEGIES[name]}`; + } + } - const overlayContent = this.$overlayContent().get(0); - const currentHeightStrategyClass = this._chooseHeightStrategy(overlayContent); + $container.removeClass(excessClasses).addClass(currentClass); + }, - this.$content().css(this._getHeightCssStyles(currentHeightStrategyClass, overlayContent)); - this._setHeightClasses(this.$overlayContent(), currentHeightStrategyClass); - }, + _isAutoHeight() { + return this.$overlayContent().get(0).style.height === 'auto'; + }, - _heightStrategyChangeOffset: function(currentHeightStrategyClass, popupVerticalPaddings) { - return currentHeightStrategyClass === HEIGHT_STRATEGIES.flex ? -popupVerticalPaddings : 0; - }, + _splitPopupHeight() { + const topToolbar = this.topToolbar(); + const bottomToolbar = this.bottomToolbar(); - _chooseHeightStrategy: function(overlayContent) { - const isAutoWidth = overlayContent.style.width === 'auto' || overlayContent.style.width === ''; - let currentHeightStrategyClass = HEIGHT_STRATEGIES.static; + return { + header: getVisibleHeight(topToolbar && topToolbar.get(0)), + footer: getVisibleHeight(bottomToolbar && bottomToolbar.get(0)), + contentVerticalOffsets: getVerticalOffsets(this.$overlayContent().get(0), true), + popupVerticalOffsets: getVerticalOffsets(this.$content().get(0), true), + popupVerticalPaddings: getVerticalOffsets(this.$content().get(0), false), + }; + }, + + _isAllWindowCovered() { + return this.callBase() || this.option('fullScreen'); + }, + + _renderDimensions() { + if (this.option('fullScreen')) { + this.$overlayContent().css({ + width: '100%', + height: '100%', + minWidth: '', + maxWidth: '', + minHeight: '', + maxHeight: '', + }); + } else { + this.callBase(); + } + if (hasWindow()) { + this._renderFullscreenWidthClass(); + } + }, - if(this._isAutoHeight() && this.option('autoResizeEnabled')) { - if(isAutoWidth || IS_OLD_SAFARI) { - currentHeightStrategyClass = HEIGHT_STRATEGIES.inherit; - } else { - currentHeightStrategyClass = HEIGHT_STRATEGIES.flex; - } - } + _dimensionChanged() { + this._renderGeometry({ isDimensionChange: true }); + }, - return currentHeightStrategyClass; - }, + _clean() { + this.callBase(); + this._observeContentResize(false); + }, - _getHeightCssStyles: function(currentHeightStrategyClass, overlayContent) { - let cssStyles = {}; - const contentMaxHeight = this._getOptionValue('maxHeight', overlayContent); - const contentMinHeight = this._getOptionValue('minHeight', overlayContent); - const popupHeightParts = this._splitPopupHeight(); - const toolbarsAndVerticalOffsetsHeight = popupHeightParts.header - + popupHeightParts.footer - + popupHeightParts.contentVerticalOffsets - + popupHeightParts.popupVerticalOffsets - + this._heightStrategyChangeOffset(currentHeightStrategyClass, popupHeightParts.popupVerticalPaddings); + _dispose() { + this.callBase(); - if(currentHeightStrategyClass === HEIGHT_STRATEGIES.static) { - if(!this._isAutoHeight() || contentMaxHeight || contentMinHeight) { - const overlayHeight = this.option('fullScreen') - ? Math.min(getBoundingRect(overlayContent).height, getWindow().innerHeight) - : getBoundingRect(overlayContent).height; - const contentHeight = overlayHeight - toolbarsAndVerticalOffsetsHeight; - cssStyles = { - height: Math.max(0, contentHeight), - minHeight: 'auto', - maxHeight: 'auto' - }; - } - } else { - const container = $(this._positionController.$visualContainer).get(0); - const maxHeightValue = addOffsetToMaxHeight(contentMaxHeight, -toolbarsAndVerticalOffsetsHeight, container); - const minHeightValue = addOffsetToMinHeight(contentMinHeight, -toolbarsAndVerticalOffsetsHeight, container); - - cssStyles = { - height: 'auto', - minHeight: minHeightValue, - maxHeight: maxHeightValue - }; - } + this._toggleBodyScroll(true); + }, - return cssStyles; - }, + _renderFullscreenWidthClass() { + this.$overlayContent().toggleClass(POPUP_FULL_SCREEN_WIDTH_CLASS, getOuterWidth(this.$overlayContent()) === getWidth(window)); + }, - _setHeightClasses: function($container, currentClass) { - let excessClasses = ''; + _toggleSafariScrolling() { + if (!this.option('enableBodyScroll')) { + return; + } - for(const name in HEIGHT_STRATEGIES) { - if(HEIGHT_STRATEGIES[name] !== currentClass) { - excessClasses += ' ' + HEIGHT_STRATEGIES[name]; - } - } + this.callBase(); + }, - $container.removeClass(excessClasses).addClass(currentClass); - }, + _toggleBodyScroll(enabled) { + if (!this._bodyOverflowManager) { + return; + } - _isAutoHeight: function() { - return this.$overlayContent().get(0).style.height === 'auto'; - }, + const { setOverflow, restoreOverflow } = this._bodyOverflowManager; - _splitPopupHeight: function() { - const topToolbar = this.topToolbar(); - const bottomToolbar = this.bottomToolbar(); + if (enabled) { + restoreOverflow(); + } else { + setOverflow(); + } + }, - return { - header: getVisibleHeight(topToolbar && topToolbar.get(0)), - footer: getVisibleHeight(bottomToolbar && bottomToolbar.get(0)), - contentVerticalOffsets: getVerticalOffsets(this.$overlayContent().get(0), true), - popupVerticalOffsets: getVerticalOffsets(this.$content().get(0), true), - popupVerticalPaddings: getVerticalOffsets(this.$content().get(0), false) - }; - }, + refreshPosition() { + this._renderPosition(); + }, - _isAllWindowCovered: function() { - return this.callBase() || this.option('fullScreen'); - }, + _optionChanged(args) { + const { value, name } = args; - _renderDimensions: function() { - if(this.option('fullScreen')) { - this.$overlayContent().css({ - width: '100%', - height: '100%', - minWidth: '', - maxWidth: '', - minHeight: '', - maxHeight: '' - }); - } else { - this.callBase(); + switch (name) { + case 'disabled': + this.callBase(args); + this._renderTitle(); + this._renderBottom(); + break; + case 'animation': + this._updateResizeCallbackSkipCondition(); + break; + case 'enableBodyScroll': + if (this.option('visible')) { + this._toggleBodyScroll(value); } - if(hasWindow()) { - this._renderFullscreenWidthClass(); + break; + case 'showTitle': + case 'title': + case 'titleTemplate': + this._renderTitle(); + this._renderGeometry(); + triggerResizeEvent(this.$overlayContent()); + break; + case 'bottomTemplate': + this._renderBottom(); + this._renderGeometry(); + triggerResizeEvent(this.$overlayContent()); + break; + case 'container': + this.callBase(args); + if (this.option('resizeEnabled')) { + this._resizable?.option('area', this._positionController.$dragResizeContainer); } - }, - - _dimensionChanged: function() { - this._renderGeometry({ isDimensionChange: true }); - }, + break; + case 'width': + case 'height': + this.callBase(args); + this._resizable?.option(name, value); + break; + case 'onTitleRendered': + this._createTitleRenderAction(value); + break; + case 'toolbarItems': + case 'useDefaultToolbarButtons': + case 'useFlatToolbarButtons': { + // NOTE: Geometry rendering after "toolbarItems" runtime change breaks the popup animation first appereance. + // But geometry rendering for options connected to the popup position still should be called. + const shouldRenderGeometry = !args.fullName.match(/^toolbarItems((\[\d+\])(\.(options|visible).*)?)?$/); - _clean: function() { - this.callBase(); - this._observeContentResize(false); - }, - - _dispose: function() { - this.callBase(); - - this._toggleBodyScroll(true); - }, - - _renderFullscreenWidthClass: function() { - this.$overlayContent().toggleClass(POPUP_FULL_SCREEN_WIDTH_CLASS, getOuterWidth(this.$overlayContent()) === getWidth(window)); - }, + this._renderTitle(); + this._renderBottom(); - _toggleSafariScrolling() { - if(!this.option('enableBodyScroll')) { - return; + if (shouldRenderGeometry) { + this._renderGeometry(); + triggerResizeEvent(this.$overlayContent()); } - - this.callBase(); - }, - - _toggleBodyScroll: function(enabled) { - if(!this._bodyOverflowManager) { - return; + break; + } + case 'dragEnabled': + this._renderDrag(); + break; + case 'dragAndResizeArea': + this._positionController.dragAndResizeArea = value; + if (this.option('resizeEnabled')) { + this._resizable.option('area', this._positionController.$dragResizeContainer); } - - const { setOverflow, restoreOverflow } = this._bodyOverflowManager; - - if(enabled) { - restoreOverflow(); - } else { - setOverflow(); + this._positionController.positionContent(); + break; + case 'dragOutsideBoundary': + this._positionController.dragOutsideBoundary = value; + if (this.option('resizeEnabled')) { + this._resizable.option('area', this._positionController.$dragResizeContainer); } - }, - - refreshPosition: function() { - this._renderPosition(); - }, + break; + case 'outsideDragFactor': + this._positionController.outsideDragFactor = value; + break; + case 'resizeEnabled': + this._renderResize(); + this._renderGeometry(); + break; + case 'autoResizeEnabled': + this._renderGeometry(); + triggerResizeEvent(this.$overlayContent()); + break; + case 'fullScreen': + this._positionController.fullScreen = value; + + this._toggleFullScreenClass(value); + this._toggleSafariScrolling(); + + this._renderGeometry(); + triggerResizeEvent(this.$overlayContent()); + break; + case 'showCloseButton': + this._renderTitle(); + break; + case 'preventScrollEvents': + this.callBase(args); - _optionChanged: function(args) { - const { value, name } = args; - - switch(name) { - case 'disabled': - this.callBase(args); - this._renderTitle(); - this._renderBottom(); - break; - case 'animation': - this._updateResizeCallbackSkipCondition(); - break; - case 'enableBodyScroll': - if(this.option('visible')) { - this._toggleBodyScroll(value); - } - break; - case 'showTitle': - case 'title': - case 'titleTemplate': - this._renderTitle(); - this._renderGeometry(); - triggerResizeEvent(this.$overlayContent()); - break; - case 'bottomTemplate': - this._renderBottom(); - this._renderGeometry(); - triggerResizeEvent(this.$overlayContent()); - break; - case 'container': - this.callBase(args); - if(this.option('resizeEnabled')) { - this._resizable?.option('area', this._positionController.$dragResizeContainer); - } - break; - case 'width': - case 'height': - this.callBase(args); - this._resizable?.option(name, value); - break; - case 'onTitleRendered': - this._createTitleRenderAction(value); - break; - case 'toolbarItems': - case 'useDefaultToolbarButtons': - case 'useFlatToolbarButtons': { - // NOTE: Geometry rendering after "toolbarItems" runtime change breaks the popup animation first appereance. - // But geometry rendering for options connected to the popup position still should be called. - const shouldRenderGeometry = !args.fullName.match(/^toolbarItems((\[\d+\])(\.(options|visible).*)?)?$/); - - this._renderTitle(); - this._renderBottom(); - - if(shouldRenderGeometry) { - this._renderGeometry(); - triggerResizeEvent(this.$overlayContent()); - } - break; - } - case 'dragEnabled': - this._renderDrag(); - break; - case 'dragAndResizeArea': - this._positionController.dragAndResizeArea = value; - if(this.option('resizeEnabled')) { - this._resizable.option('area', this._positionController.$dragResizeContainer); - } - this._positionController.positionContent(); - break; - case 'dragOutsideBoundary': - this._positionController.dragOutsideBoundary = value; - if(this.option('resizeEnabled')) { - this._resizable.option('area', this._positionController.$dragResizeContainer); - } - break; - case 'outsideDragFactor': - this._positionController.outsideDragFactor = value; - break; - case 'resizeEnabled': - this._renderResize(); - this._renderGeometry(); - break; - case 'autoResizeEnabled': - this._renderGeometry(); - triggerResizeEvent(this.$overlayContent()); - break; - case 'fullScreen': - this._positionController.fullScreen = value; - - this._toggleFullScreenClass(value); - this._toggleSafariScrolling(); - - this._renderGeometry(); - triggerResizeEvent(this.$overlayContent()); - break; - case 'showCloseButton': - this._renderTitle(); - break; - case 'preventScrollEvents': - this.callBase(args); - - this._toggleContentScrollClass(); - break; - default: - this.callBase(args); - } - }, + this._toggleContentScrollClass(); + break; + default: + this.callBase(args); + } + }, - bottomToolbar: function() { - return this._$bottom; - }, + bottomToolbar() { + return this._$bottom; + }, - topToolbar: function() { - return this._$title; - }, + topToolbar() { + return this._$title; + }, - $content: function() { - return this._$popupContent; - }, + $content() { + return this._$popupContent; + }, - content: function() { - return getPublicElement(this.$content()); - }, + content() { + return getPublicElement(this.$content()); + }, - $overlayContent: function() { - return this._$content; - }, + $overlayContent() { + return this._$content; + }, - getFocusableElements: function() { - return this.$wrapper().find('[tabindex]').filter((index, item) => { - return item.getAttribute('tabindex') >= 0; - }); - } + getFocusableElements() { + return this.$wrapper().find('[tabindex]').filter((index, item) => item.getAttribute('tabindex') >= 0); + }, }); registerComponent('dxPopup', Popup); diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup_drag.ts b/packages/devextreme/js/__internal/ui/popup/m_popup_drag.ts index 6e4373399ece..73ce016aa3e8 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup_drag.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup_drag.ts @@ -1,234 +1,249 @@ -import { locate, move } from '../../animation/translator'; -import domAdapter from '../../core/dom_adapter'; -import { getOffset, getOuterWidth, getOuterHeight } from '../../core/utils/size'; -import { fitIntoRange } from '../../core/utils/math'; -import { isWindow } from '../../core/utils/type'; -import eventsEngine from '../../events/core/events_engine'; +import { locate, move } from '@js/animation/translator'; +import domAdapter from '@js/core/dom_adapter'; +import { fitIntoRange } from '@js/core/utils/math'; +import { getOffset, getOuterHeight, getOuterWidth } from '@js/core/utils/size'; +import { isWindow } from '@js/core/utils/type'; +import eventsEngine from '@js/events/core/events_engine'; import { - start as dragStartEvent, - move as dragMoveEvent, - end as dragEndEvent -} from '../../events/drag'; -import { addNamespace } from '../../events/utils/index'; + end as dragEndEvent, + move as dragMoveEvent, + start as dragStartEvent, +} from '@js/events/drag'; +import { addNamespace } from '@js/events/utils/index'; const KEYBOARD_DRAG_STEP = 5; class PopupDrag { - constructor(config) { - this.init(config); - } + _positionController: any; - init({ dragEnabled, handle, draggableElement, positionController }) { - // TODO: get rid of dragEnabled + _draggableElement: any; - this._positionController = positionController; - this._draggableElement = draggableElement; - this._handle = handle; - this._dragEnabled = dragEnabled; + _handle: any; - this.unsubscribe(); + _dragEnabled: any; - if(!dragEnabled) { - return; - } + _prevOffset!: { x: number; y: number }; - this.subscribe(); - } + constructor(config) { + this.init(config); + } - moveDown(e) { - this._moveTo(KEYBOARD_DRAG_STEP, 0, e); - } + init({ + dragEnabled, + handle, + draggableElement, + positionController, + }) { + // TODO: get rid of dragEnabled - moveUp(e) { - this._moveTo(-KEYBOARD_DRAG_STEP, 0, e); - } + this._positionController = positionController; + this._draggableElement = draggableElement; + this._handle = handle; + this._dragEnabled = dragEnabled; - moveLeft(e) { - this._moveTo(0, -KEYBOARD_DRAG_STEP, e); - } + this.unsubscribe(); - moveRight(e) { - this._moveTo(0, KEYBOARD_DRAG_STEP, e); + if (!dragEnabled) { + return; } - subscribe() { - const eventNames = this._getEventNames(); + this.subscribe(); + } - eventsEngine.on(this._handle, eventNames.startEventName, (e) => { this._dragStartHandler(e); }); - eventsEngine.on(this._handle, eventNames.updateEventName, (e) => { this._dragUpdateHandler(e); }); - eventsEngine.on(this._handle, eventNames.endEventName, (e) => { this._dragEndHandler(e); }); - } + moveDown(e) { + this._moveTo(KEYBOARD_DRAG_STEP, 0, e); + } - unsubscribe() { - const eventNames = this._getEventNames(); + moveUp(e) { + this._moveTo(-KEYBOARD_DRAG_STEP, 0, e); + } - eventsEngine.off(this._handle, eventNames.startEventName); - eventsEngine.off(this._handle, eventNames.updateEventName); - eventsEngine.off(this._handle, eventNames.endEventName); - } + moveLeft(e) { + this._moveTo(0, -KEYBOARD_DRAG_STEP, e); + } - _getEventNames() { - const namespace = 'overlayDrag'; - const startEventName = addNamespace(dragStartEvent, namespace); - const updateEventName = addNamespace(dragMoveEvent, namespace); - const endEventName = addNamespace(dragEndEvent, namespace); - - return { - startEventName, - updateEventName, - endEventName - }; - } + moveRight(e) { + this._moveTo(0, KEYBOARD_DRAG_STEP, e); + } - _dragStartHandler(e) { - const allowedOffsets = this._getAllowedOffsets(); + subscribe() { + const eventNames = this._getEventNames(); - this._prevOffset = { x: 0, y: 0 }; + eventsEngine.on(this._handle, eventNames.startEventName, (e) => { this._dragStartHandler(e); }); + eventsEngine.on(this._handle, eventNames.updateEventName, (e) => { this._dragUpdateHandler(e); }); + eventsEngine.on(this._handle, eventNames.endEventName, (e) => { this._dragEndHandler(e); }); + } - e.targetElements = []; - e.maxTopOffset = allowedOffsets.top; - e.maxBottomOffset = allowedOffsets.bottom; - e.maxLeftOffset = allowedOffsets.left; - e.maxRightOffset = allowedOffsets.right; - } + unsubscribe() { + const eventNames = this._getEventNames(); - _dragUpdateHandler(e) { - const targetOffset = { - top: e.offset.y - this._prevOffset.y, - left: e.offset.x - this._prevOffset.x - }; + eventsEngine.off(this._handle, eventNames.startEventName); + eventsEngine.off(this._handle, eventNames.updateEventName); + eventsEngine.off(this._handle, eventNames.endEventName); + } - this._moveByOffset(targetOffset); + _getEventNames() { + const namespace = 'overlayDrag'; + const startEventName = addNamespace(dragStartEvent, namespace); + const updateEventName = addNamespace(dragMoveEvent, namespace); + const endEventName = addNamespace(dragEndEvent, namespace); - this._prevOffset = e.offset; - } + return { + startEventName, + updateEventName, + endEventName, + }; + } - _dragEndHandler(event) { - this._positionController.dragHandled(); - this._positionController.detectVisualPositionChange(event); - } - - _moveTo(top, left, e) { - if(!this._dragEnabled) { - return; - } + _dragStartHandler(e) { + const allowedOffsets = this._getAllowedOffsets(); - e.preventDefault(); - e.stopPropagation(); + this._prevOffset = { x: 0, y: 0 }; - const offset = this._fitOffsetIntoAllowedRange(top, left); - this._moveByOffset(offset); - this._dragEndHandler(e); - } + e.targetElements = []; + e.maxTopOffset = allowedOffsets.top; + e.maxBottomOffset = allowedOffsets.bottom; + e.maxLeftOffset = allowedOffsets.left; + e.maxRightOffset = allowedOffsets.right; + } - _fitOffsetIntoAllowedRange(top, left) { - const allowedOffsets = this._getAllowedOffsets(); - - return { - top: fitIntoRange(top, -allowedOffsets.top, allowedOffsets.bottom), - left: fitIntoRange(left, -allowedOffsets.left, allowedOffsets.right) - }; - } + _dragUpdateHandler(e) { + const targetOffset = { + top: e.offset.y - this._prevOffset.y, + left: e.offset.x - this._prevOffset.x, + }; - _getContainerDimensions() { - const document = domAdapter.getDocument(); - const container = this._positionController.$dragResizeContainer.get(0); - - let containerWidth = getOuterWidth(container); - let containerHeight = getOuterHeight(container); - if(isWindow(container)) { - containerHeight = Math.max(document.body.clientHeight, containerHeight); - containerWidth = Math.max(document.body.clientWidth, containerWidth); - } - - return { - width: containerWidth, - height: containerHeight - }; - } - - _getContainerPosition() { - const container = this._positionController.$dragResizeContainer.get(0); - - return isWindow(container) - ? { top: 0, left: 0 } - : getOffset(container); - } - - _getElementPosition() { - return getOffset(this._draggableElement); - } - - _getInnerDelta() { - const containerDimensions = this._getContainerDimensions(); - const elementDimensions = this._getElementDimensions(); - - return { - x: containerDimensions.width - elementDimensions.width, - y: containerDimensions.height - elementDimensions.height, - }; - } - - _getOuterDelta() { - const { width, height } = this._getElementDimensions(); - const outsideDragFactor = this._positionController.outsideDragFactor; - - return { - x: width * outsideDragFactor, - y: height * outsideDragFactor - }; - } - - _getFullDelta() { - const fullDelta = this._getInnerDelta(); - const outerDelta = this._getOuterDelta(); - - return { - x: fullDelta.x + outerDelta.x, - y: fullDelta.y + outerDelta.y, - }; - } - - _getElementDimensions() { - return { - width: this._draggableElement.offsetWidth, - height: this._draggableElement.offsetHeight - }; - } - - _getAllowedOffsets() { - const fullDelta = this._getFullDelta(); - const isDragAllowed = fullDelta.y >= 0 && fullDelta.x >= 0; - if(!isDragAllowed) { - return { - top: 0, - bottom: 0, - left: 0, - right: 0 - }; - } - - const elementPosition = this._getElementPosition(); - const containerPosition = this._getContainerPosition(); - const outerDelta = this._getOuterDelta(); - - return { - top: elementPosition.top - containerPosition.top + outerDelta.y, - bottom: -elementPosition.top + containerPosition.top + fullDelta.y, - left: elementPosition.left - containerPosition.left + outerDelta.x, - right: -elementPosition.left + containerPosition.left + fullDelta.x - }; - } - - _moveByOffset(offset) { - const currentPosition = locate(this._draggableElement); - const newPosition = { - left: currentPosition.left + offset.left, - top: currentPosition.top + offset.top - }; - - move(this._draggableElement, newPosition); - } + this._moveByOffset(targetOffset); + + this._prevOffset = e.offset; + } + + _dragEndHandler(event) { + this._positionController.dragHandled(); + this._positionController.detectVisualPositionChange(event); + } + + _moveTo(top, left, e) { + if (!this._dragEnabled) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + const offset = this._fitOffsetIntoAllowedRange(top, left); + this._moveByOffset(offset); + this._dragEndHandler(e); + } + + _fitOffsetIntoAllowedRange(top, left) { + const allowedOffsets = this._getAllowedOffsets(); + + return { + top: fitIntoRange(top, -allowedOffsets.top, allowedOffsets.bottom), + left: fitIntoRange(left, -allowedOffsets.left, allowedOffsets.right), + }; + } + + _getContainerDimensions() { + const document = domAdapter.getDocument(); + const container = this._positionController.$dragResizeContainer.get(0); + + let containerWidth = getOuterWidth(container); + let containerHeight = getOuterHeight(container); + if (isWindow(container)) { + containerHeight = Math.max(document.body.clientHeight, containerHeight); + containerWidth = Math.max(document.body.clientWidth, containerWidth); + } + + return { + width: containerWidth, + height: containerHeight, + }; + } + + _getContainerPosition() { + const container = this._positionController.$dragResizeContainer.get(0); + + return isWindow(container) + ? { top: 0, left: 0 } + : getOffset(container); + } + + _getElementPosition() { + return getOffset(this._draggableElement); + } + + _getInnerDelta() { + const containerDimensions = this._getContainerDimensions(); + const elementDimensions = this._getElementDimensions(); + + return { + x: containerDimensions.width - elementDimensions.width, + y: containerDimensions.height - elementDimensions.height, + }; + } + + _getOuterDelta() { + const { width, height } = this._getElementDimensions(); + const { outsideDragFactor } = this._positionController; + + return { + x: width * outsideDragFactor, + y: height * outsideDragFactor, + }; + } + + _getFullDelta() { + const fullDelta = this._getInnerDelta(); + const outerDelta = this._getOuterDelta(); + + return { + x: fullDelta.x + outerDelta.x, + y: fullDelta.y + outerDelta.y, + }; + } + + _getElementDimensions() { + return { + width: this._draggableElement.offsetWidth, + height: this._draggableElement.offsetHeight, + }; + } + + _getAllowedOffsets() { + const fullDelta = this._getFullDelta(); + const isDragAllowed = fullDelta.y >= 0 && fullDelta.x >= 0; + if (!isDragAllowed) { + return { + top: 0, + bottom: 0, + left: 0, + right: 0, + }; + } + + const elementPosition = this._getElementPosition(); + const containerPosition = this._getContainerPosition(); + const outerDelta = this._getOuterDelta(); + + return { + top: elementPosition.top - containerPosition.top + outerDelta.y, + bottom: -elementPosition.top + containerPosition.top + fullDelta.y, + left: elementPosition.left - containerPosition.left + outerDelta.x, + right: -elementPosition.left + containerPosition.left + fullDelta.x, + }; + } + + _moveByOffset(offset) { + const currentPosition = locate(this._draggableElement); + const newPosition = { + left: currentPosition.left + offset.left, + top: currentPosition.top + offset.top, + }; + + move(this._draggableElement, newPosition); + } } export default PopupDrag; diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup_overflow_manager.ts b/packages/devextreme/js/__internal/ui/popup/m_popup_overflow_manager.ts index f4f0c7704686..ab16e450cc35 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup_overflow_manager.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup_overflow_manager.ts @@ -1,136 +1,147 @@ -import { getWindow, hasWindow } from '../../core/utils/window'; -import { isDefined } from '../../core/utils/type'; -import domAdapter from '../../core/dom_adapter'; -import devices from '../../core/devices'; -import { noop } from '../../core/utils/common'; - -const overflowManagerMock = { - setOverflow: noop, - restoreOverflow: noop, +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import { noop } from '@js/core/utils/common'; +import { isDefined } from '@js/core/utils/type'; +import { getWindow, hasWindow } from '@js/core/utils/window'; + +const overflowManagerMock: { + setOverflow: () => void; + restoreOverflow: () => void; +} = { + setOverflow: noop, + restoreOverflow: noop, }; -export const createBodyOverflowManager = () => { - if(!hasWindow()) { - return overflowManagerMock; +export const createBodyOverflowManager = (): typeof overflowManagerMock => { + if (!hasWindow()) { + return overflowManagerMock; + } + + const window = getWindow(); + const { documentElement } = domAdapter.getDocument(); + const body = domAdapter.getBody(); + + const isIosDevice = devices.real().platform === 'ios'; + + const prevSettings: { + overflow: string | null; + overflowX: string | null; + overflowY: string | null; + paddingRight: number | null; + position: string | null; + top: string | null; + left: string | null; + } = { + overflow: null, + overflowX: null, + overflowY: null, + paddingRight: null, + position: null, + top: null, + left: null, + }; + + const setBodyPositionFixed = (): void => { + if (isDefined(prevSettings.position) || body.style.position === 'fixed') { + return; } - const window = getWindow(); - const documentElement = domAdapter.getDocument().documentElement; - const body = domAdapter.getBody(); - - const isIosDevice = devices.real().platform === 'ios'; - - const prevSettings = { - overflow: null, - overflowX: null, - overflowY: null, - paddingRight: null, - position: null, - top: null, - left: null, - }; - - const setBodyPositionFixed = () => { - if(isDefined(prevSettings.position) || body.style.position === 'fixed') { - return; - } - - const { scrollY, scrollX } = window; - - prevSettings.position = body.style.position; - prevSettings.top = body.style.top; - prevSettings.left = body.style.left; - - body.style.setProperty('position', 'fixed'); - body.style.setProperty('top', `${-scrollY}px`); - body.style.setProperty('left', `${-scrollX}px`); - }; - - const restoreBodyPositionFixed = () => { - if(!isDefined(prevSettings.position)) { - return; - } - - const scrollY = -parseInt(body.style.top, 10); - const scrollX = -parseInt(body.style.left, 10); - - ['position', 'top', 'left'].forEach((property) => { - if(prevSettings[property]) { - body.style.setProperty(property, prevSettings[property]); - } else { - body.style.removeProperty(property); - } - }); - - window.scrollTo(scrollX, scrollY); - - prevSettings.position = null; - }; - - const setBodyOverflow = () => { - setBodyPaddingRight(); - - if(prevSettings.overflow || body.style.overflow === 'hidden') { - return; - } - - prevSettings.overflow = body.style.overflow; - prevSettings.overflowX = body.style.overflowX; - prevSettings.overflowY = body.style.overflowY; - - body.style.setProperty('overflow', 'hidden'); - }; - - const restoreBodyOverflow = () => { - restoreBodyPaddingRight(); - - ['overflow', 'overflowX', 'overflowY'].forEach((property) => { - if(!isDefined(prevSettings[property])) { - return; - } - const propertyInKebabCase = property.replace(/(X)|(Y)/, (symbol) => `-${symbol.toLowerCase()}`); - if(prevSettings[property]) { - body.style.setProperty(propertyInKebabCase, prevSettings[property]); - } else { - body.style.removeProperty(propertyInKebabCase); - } - prevSettings[property] = null; - }); - }; - - const setBodyPaddingRight = () => { - const scrollBarWidth = window.innerWidth - documentElement.clientWidth; - if(prevSettings.paddingRight || scrollBarWidth <= 0) { - return; - } - - const paddingRight = window.getComputedStyle(body).getPropertyValue('padding-right'); - const computedBodyPaddingRight = parseInt(paddingRight, 10); - prevSettings.paddingRight = computedBodyPaddingRight; - - body.style.setProperty('padding-right', `${computedBodyPaddingRight + scrollBarWidth}px`); - }; - - const restoreBodyPaddingRight = () => { - if(!isDefined(prevSettings.paddingRight)) { - return; - } - - if(prevSettings.paddingRight) { - body.style.setProperty('padding-right', `${prevSettings.paddingRight}px`); - } else { - body.style.removeProperty('padding-right'); - } - - prevSettings.paddingRight = null; - }; - - return { - setOverflow: isIosDevice - ? setBodyPositionFixed - : setBodyOverflow, - restoreOverflow: isIosDevice - ? restoreBodyPositionFixed - : restoreBodyOverflow, - }; + const { scrollY, scrollX } = window; + + prevSettings.position = body.style.position; + prevSettings.top = body.style.top; + prevSettings.left = body.style.left; + + body.style.setProperty('position', 'fixed'); + body.style.setProperty('top', `${-scrollY}px`); + body.style.setProperty('left', `${-scrollX}px`); + }; + + const restoreBodyPositionFixed = (): void => { + if (!isDefined(prevSettings.position)) { + return; + } + + const scrollY = -parseInt(body.style.top, 10); + const scrollX = -parseInt(body.style.left, 10); + + ['position', 'top', 'left'].forEach((property) => { + if (prevSettings[property]) { + body.style.setProperty(property, prevSettings[property]); + } else { + body.style.removeProperty(property); + } + }); + + window.scrollTo(scrollX, scrollY); + + prevSettings.position = null; + }; + + const setBodyPaddingRight = (): void => { + const scrollBarWidth = window.innerWidth - documentElement.clientWidth; + if (prevSettings.paddingRight || scrollBarWidth <= 0) { + return; + } + + const paddingRight = window.getComputedStyle(body).getPropertyValue('padding-right'); + const computedBodyPaddingRight = parseInt(paddingRight, 10); + prevSettings.paddingRight = computedBodyPaddingRight; + + body.style.setProperty('padding-right', `${computedBodyPaddingRight + scrollBarWidth}px`); + }; + + const setBodyOverflow = (): void => { + setBodyPaddingRight(); + + if (prevSettings.overflow || body.style.overflow === 'hidden') { + return; + } + + prevSettings.overflow = body.style.overflow; + prevSettings.overflowX = body.style.overflowX; + prevSettings.overflowY = body.style.overflowY; + + body.style.setProperty('overflow', 'hidden'); + }; + + const restoreBodyPaddingRight = (): void => { + if (!isDefined(prevSettings.paddingRight)) { + return; + } + + if (prevSettings.paddingRight) { + body.style.setProperty('padding-right', `${prevSettings.paddingRight}px`); + } else { + body.style.removeProperty('padding-right'); + } + + prevSettings.paddingRight = null; + }; + + const restoreBodyOverflow = (): void => { + restoreBodyPaddingRight(); + + ['overflow', 'overflowX', 'overflowY'].forEach((property) => { + if (!isDefined(prevSettings[property])) { + return; + } + const propertyInKebabCase = property.replace(/(X)|(Y)/, (symbol) => `-${symbol.toLowerCase()}`); + if (prevSettings[property]) { + body.style.setProperty(propertyInKebabCase, prevSettings[property]); + } else { + body.style.removeProperty(propertyInKebabCase); + } + prevSettings[property] = null; + }); + }; + + return { + setOverflow: isIosDevice + ? setBodyPositionFixed + : setBodyOverflow, + restoreOverflow: isIosDevice + ? restoreBodyPositionFixed + : restoreBodyOverflow, + }; }; diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts b/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts index e6005c0da679..100c335177b0 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts @@ -1,129 +1,142 @@ -import $ from '../../core/renderer'; -import { move } from '../../animation/translator'; -import { getWindow } from '../../core/utils/window'; -import { originalViewPort } from '../../core/utils/view_port'; -import { OverlayPositionController } from '../../__internal/ui/overlay/m_overlay_position_controller'; +import { move } from '@js/animation/translator'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { originalViewPort } from '@js/core/utils/view_port'; +import { getWindow } from '@js/core/utils/window'; +import { OverlayPositionController } from '@ts/ui/overlay/m_overlay_position_controller'; const window = getWindow(); class PopupPositionController extends OverlayPositionController { - constructor({ - fullScreen, forceApplyBindings, - dragOutsideBoundary, dragAndResizeArea, outsideDragFactor, - ...args - }) { - super(args); - - this._props = { - ...this._props, - fullScreen, - forceApplyBindings, - dragOutsideBoundary, - dragAndResizeArea, - outsideDragFactor, - }; - - this._$dragResizeContainer = undefined; - - this._updateDragResizeContainer(); - } + _$dragResizeContainer?: dxElementWrapper; - set fullScreen(fullScreen) { - this._props.fullScreen = fullScreen; + constructor({ + fullScreen, + forceApplyBindings, + dragOutsideBoundary, + dragAndResizeArea, + outsideDragFactor, + ...args + }) { + // @ts-expect-error + super(args); - if(fullScreen) { - this._fullScreenEnabled(); - } else { - this._fullScreenDisabled(); - } - } + this._props = { + ...this._props, + fullScreen, + forceApplyBindings, + dragOutsideBoundary, + dragAndResizeArea, + outsideDragFactor, + }; - get $dragResizeContainer() { - return this._$dragResizeContainer; - } + this._$dragResizeContainer = undefined; - get outsideDragFactor() { - if(this._props.dragOutsideBoundary) { - return 1; - } - - return this._props.outsideDragFactor; - } + this._updateDragResizeContainer(); + } - set dragAndResizeArea(dragAndResizeArea) { - this._props.dragAndResizeArea = dragAndResizeArea; + set fullScreen(fullScreen) { + this._props.fullScreen = fullScreen; - this._updateDragResizeContainer(); + if (fullScreen) { + this._fullScreenEnabled(); + } else { + this._fullScreenDisabled(); } + } - set dragOutsideBoundary(dragOutsideBoundary) { - this._props.dragOutsideBoundary = dragOutsideBoundary; + get $dragResizeContainer() { + return this._$dragResizeContainer; + } - this._updateDragResizeContainer(); + get outsideDragFactor() { + if (this._props.dragOutsideBoundary) { + return 1; } - set outsideDragFactor(outsideDragFactor) { - this._props.outsideDragFactor = outsideDragFactor; - } + return this._props.outsideDragFactor; + } - updateContainer(containerProp) { - super.updateContainer(containerProp); - this._updateDragResizeContainer(); - } + set dragAndResizeArea(dragAndResizeArea) { + this._props.dragAndResizeArea = dragAndResizeArea; - dragHandled() { - this.restorePositionOnNextRender(false); - } + this._updateDragResizeContainer(); + } - resizeHandled() { - this.restorePositionOnNextRender(false); - } + set dragOutsideBoundary(dragOutsideBoundary) { + this._props.dragOutsideBoundary = dragOutsideBoundary; - positionContent() { - if(this._props.fullScreen) { - move(this._$content, { top: 0, left: 0 }); - this.detectVisualPositionChange(); - } else { - this._props.forceApplyBindings?.(); + this._updateDragResizeContainer(); + } - super.positionContent(); - } - } + // eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures, grouped-accessor-pairs + set outsideDragFactor(outsideDragFactor) { + this._props.outsideDragFactor = outsideDragFactor; + } - _updateDragResizeContainer() { - this._$dragResizeContainer = this._getDragResizeContainer(); - } + updateContainer(containerProp): void { + super.updateContainer(containerProp); + this._updateDragResizeContainer(); + } + + dragHandled(): void { + this.restorePositionOnNextRender(false); + } - _getDragResizeContainer() { - if(this._props.dragOutsideBoundary) { - return $(window); - } - if(this._props.dragAndResizeArea) { - return $(this._props.dragAndResizeArea); - } + resizeHandled(): void { + this.restorePositionOnNextRender(false); + } - const isContainerDefined = originalViewPort().get(0) || this._props.container; + positionContent(): void { + if (this._props.fullScreen) { + move(this._$content, { top: 0, left: 0 }); + this.detectVisualPositionChange(); + } else { + this._props.forceApplyBindings?.(); - return isContainerDefined ? this._$markupContainer : $(window); + super.positionContent(); } + } - _getVisualContainer() { - if(this._props.fullScreen) { - return $(window); - } + _updateDragResizeContainer(): void { + this._$dragResizeContainer = this._getDragResizeContainer(); + } - return super._getVisualContainer(); + _getDragResizeContainer() { + if (this._props.dragOutsideBoundary) { + // @ts-expect-error + return $(window); } - - _fullScreenEnabled() { - this.restorePositionOnNextRender(false); + if (this._props.dragAndResizeArea) { + return $(this._props.dragAndResizeArea); } - _fullScreenDisabled() { - this.restorePositionOnNextRender(true); + const isContainerDefined = originalViewPort().get(0) || this._props.container; + + return isContainerDefined + ? this._$markupContainer + // @ts-expect-error + : $(window); + } + + _getVisualContainer(): dxElementWrapper { + if (this._props.fullScreen) { + // @ts-expect-error + return $(window); } + + return super._getVisualContainer(); + } + + _fullScreenEnabled(): void { + this.restorePositionOnNextRender(false); + } + + _fullScreenDisabled(): void { + this.restorePositionOnNextRender(true); + } } export { - PopupPositionController + PopupPositionController, }; diff --git a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts index 5adf1f5af03d..a4b3eeb760fa 100644 --- a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts @@ -2,7 +2,7 @@ import positionUtils from '@js/animation/position'; import { move } from '@js/animation/translator'; import { extend } from '@js/core/utils/extend'; import { isString } from '@js/core/utils/type'; -import { PopoverPositionController } from '@js/ui/popover/popover_position_controller'; +import { PopoverPositionController } from '@ts/ui/popover/m_popover_position_controller'; const SLIDER_TOOLTIP_POSITION_ALIASES = { top: { my: 'bottom center', at: 'top center', collision: 'fit none' }, diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts index 1ec17ff2ebdb..94c93400d8ac 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts @@ -74,7 +74,6 @@ class SpeedDialAction extends Widget { } _render(): void { - // @ts-expect-error this._toggleVisibility(false); if (!getSwatchContainer(this.$element())) { @@ -87,7 +86,6 @@ class SpeedDialAction extends Widget { _dispose() { // @ts-expect-error disposeAction(this._options.silent('id')); - // @ts-expect-error super._dispose(); } } diff --git a/packages/devextreme/js/ui/popover/ui.popover.full.js b/packages/devextreme/js/ui/popover/ui.popover.full.js new file mode 100644 index 000000000000..5319c7d98b1d --- /dev/null +++ b/packages/devextreme/js/ui/popover/ui.popover.full.js @@ -0,0 +1,3 @@ +import PopoverFull from '../../__internal/ui/popover/m_popover.full'; + +export default PopoverFull; diff --git a/packages/devextreme/js/ui/popover/ui.popover.js b/packages/devextreme/js/ui/popover/ui.popover.js new file mode 100644 index 000000000000..b5161d59f976 --- /dev/null +++ b/packages/devextreme/js/ui/popover/ui.popover.js @@ -0,0 +1,78 @@ +import Popover from '../../__internal/ui/popover/m_popover'; + +export default Popover; + +// STYLE popover + +/** + * @name dxPopoverOptions.dragEnabled + * @hidden + */ + +/** + * @name dxPopoverOptions.dragOutsideBoundary + * @hidden + */ + +/** + * @name dxPopoverOptions.dragAndResizeArea + * @hidden + */ + +/** + * @name dxPopoverOptions.resizeEnabled + * @hidden + */ + +/** + * @name dxPopoverOptions.restorePosition + * @hidden + */ + +/** + * @name dxPopoverOptions.onResizeStart + * @hidden + */ + +/** + * @name dxPopoverOptions.onResize + * @hidden + */ + +/** + * @name dxPopoverOptions.onResizeEnd + * @hidden + */ + +/** + * @name dxPopoverOptions.fullScreen + * @hidden + */ + + +/** + * @name dxPopoverOptions.focusStateEnabled + * @hidden + */ + +/** + * @name dxPopoverOptions.accessKey + * @hidden + */ + +/** + * @name dxPopoverOptions.tabIndex + * @hidden + */ + +/** + * @name dxPopover.registerKeyHandler + * @publicName registerKeyHandler(key, handler) + * @hidden + */ + +/** + * @name dxPopover.focus + * @publicName focus() + * @hidden + */ diff --git a/packages/devextreme/js/ui/popup/ui.popup.full.js b/packages/devextreme/js/ui/popup/ui.popup.full.js new file mode 100644 index 000000000000..9fe19d49ea88 --- /dev/null +++ b/packages/devextreme/js/ui/popup/ui.popup.full.js @@ -0,0 +1,3 @@ +import PopupFull from '../../__internal/ui/popup/m_popup.full'; + +export default PopupFull; diff --git a/packages/devextreme/js/ui/popup/ui.popup.js b/packages/devextreme/js/ui/popup/ui.popup.js new file mode 100644 index 000000000000..9bbf71b51818 --- /dev/null +++ b/packages/devextreme/js/ui/popup/ui.popup.js @@ -0,0 +1,5 @@ +import Popup from '../../__internal/ui/popup/m_popup'; + +export default Popup; + +// STYLE popup diff --git a/packages/devextreme/renovation.tsconfig.json b/packages/devextreme/renovation.tsconfig.json index 6223c950dec5..8d46e9bd79d8 100644 --- a/packages/devextreme/renovation.tsconfig.json +++ b/packages/devextreme/renovation.tsconfig.json @@ -7,6 +7,7 @@ "target": "ES2019", "typeRoots": [], "types": ["@types/jest"], + "noImplicitThis": false, "emitDecoratorMetadata": false, // coverage broke if true "paths": { "@js/*": [ diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popupDrag.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popupDrag.tests.js index 230f1d1300ce..a70ea5f8c92b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popupDrag.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popupDrag.tests.js @@ -1,7 +1,7 @@ import $ from 'jquery'; -import PopupDrag from 'ui/popup/popup_drag'; -import { PopupPositionController } from 'ui/popup/popup_position_controller'; +import PopupDrag from '__internal/ui/popup/m_popup_drag'; +import { PopupPositionController } from '__internal/ui/popup/m_popup_position_controller'; const KEYBOARD_DRAG_STEP = 5; From 2b9a6558e26a53c2fcfd6c1bfa11c62cbcfdd64d Mon Sep 17 00:00:00 2001 From: AlexanderMoiseev Date: Wed, 12 Jun 2024 20:27:22 +0400 Subject: [PATCH 27/32] Chart: recreate legend's title to prevent caching leading to stale values (T1210271) (#27498) --- .../devextreme/js/viz/components/legend.js | 4 +- .../chartInteraction.tests.js | 39 +++++++++++++++++++ .../DevExpress.viz.vectorMap/legend.tests.js | 2 +- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/viz/components/legend.js b/packages/devextreme/js/viz/components/legend.js index a9bf7ccb4fdf..98f891a55374 100644 --- a/packages/devextreme/js/viz/components/legend.js +++ b/packages/devextreme/js/viz/components/legend.js @@ -423,7 +423,9 @@ extend(legendPrototype, { y: 0 }; - if(that.isVisible() && !that._title) { + if(that.isVisible()) { + that._title?.dispose(); + that._title = new Title({ renderer: that._renderer, cssClass: that._titleGroupClass, root: that._legendGroup }); } diff --git a/packages/devextreme/testing/tests/DevExpress.viz.charts/chartInteraction.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.charts/chartInteraction.tests.js index 451709e36421..7267ab5c4d8b 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.charts/chartInteraction.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.charts/chartInteraction.tests.js @@ -7,6 +7,7 @@ import 'viz/chart'; import 'viz/polar_chart'; const SERIES_POINT_MARKER_SELECTOR = '.dxc-series circle'; +const LEGEND_TEXT_SELECTOR = '.dxc-legend .dxc-title text'; QUnit.testStart(function() { const markup = @@ -223,6 +224,44 @@ QUnit.test('Legend\'s title as string', function(assert) { assert.strictEqual(drawn.callCount, 1); }); +QUnit.test('Legend title position should not change after legend visibility change (T1210271)', function(assert) { + const chart = $('#chart').dxChart({ + legend: { + title: 'Legend', + visible: true + }, + series: [{}], + }); + + const initialTextY = Number(chart.find(LEGEND_TEXT_SELECTOR).attr('y')); + + assert.roughEqual(initialTextY, 17, 2); + + const chartInstance = chart.dxChart('instance'); + chartInstance.option('legend.visible', false); + chartInstance.option('legend.visible', true); + + const textYAfterVisibilityChange = Number(chart.find(LEGEND_TEXT_SELECTOR).attr('y')); + assert.roughEqual(textYAfterVisibilityChange, 17, 2); +}); + +QUnit.test('Old title should be disposed upon creating a new one', function(assert) { + const chart = $('#chart').dxChart({ + legend: { + title: 'Legend', + visible: true + }, + series: [{}], + }).dxChart('instance'); + + const disposeSpy = sinon.spy(chart._legend._title, 'dispose'); + + chart.option('legend.visible', false); + chart.option('legend.visible', true); + + assert.strictEqual(disposeSpy.callCount, 1); +}); + // T999609 QUnit.test('Value axis range ajusting after resetVisualRange', function(assert) { const dataSource = []; diff --git a/packages/devextreme/testing/tests/DevExpress.viz.vectorMap/legend.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.vectorMap/legend.tests.js index f596c7fc0e1b..b36ab6c93068 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.vectorMap/legend.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.vectorMap/legend.tests.js @@ -182,7 +182,7 @@ QUnit.test('resize', function(assert) { this.legend.resize({ width: 300, height: 100 }); - assert.strictEqual(this.renderer.g.callCount, 18, 'redrawn'); + assert.strictEqual(this.renderer.g.callCount, 19, 'redrawn'); assert.strictEqual(this.notifyDirty.callCount, 1, 'notify dirty'); assert.strictEqual(this.notifyReady.callCount, 1, 'notify ready'); }); From dd076141fe978b47e1212055025c5dec112d17aa Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 18:15:15 +0400 Subject: [PATCH 28/32] Validator, ValidationMessage, ValidationSummary, ValidationEngine, ValidationGroup: move files to TS --- .../validation_engine.js => __internal/ui/m_validation_engine.ts} | 0 .../validation_group.js => __internal/ui/m_validation_group.ts} | 0 .../ui/m_validation_message.ts} | 0 .../ui/m_validation_summary.ts} | 0 .../js/{ui/validator.js => __internal/ui/m_validator.ts} | 0 .../ui/validation/m_default_adapter.ts} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/validation_engine.js => __internal/ui/m_validation_engine.ts} (100%) rename packages/devextreme/js/{ui/validation_group.js => __internal/ui/m_validation_group.ts} (100%) rename packages/devextreme/js/{ui/validation_message.js => __internal/ui/m_validation_message.ts} (100%) rename packages/devextreme/js/{ui/validation_summary.js => __internal/ui/m_validation_summary.ts} (100%) rename packages/devextreme/js/{ui/validator.js => __internal/ui/m_validator.ts} (100%) rename packages/devextreme/js/{ui/validation/default_adapter.js => __internal/ui/validation/m_default_adapter.ts} (100%) diff --git a/packages/devextreme/js/ui/validation_engine.js b/packages/devextreme/js/__internal/ui/m_validation_engine.ts similarity index 100% rename from packages/devextreme/js/ui/validation_engine.js rename to packages/devextreme/js/__internal/ui/m_validation_engine.ts diff --git a/packages/devextreme/js/ui/validation_group.js b/packages/devextreme/js/__internal/ui/m_validation_group.ts similarity index 100% rename from packages/devextreme/js/ui/validation_group.js rename to packages/devextreme/js/__internal/ui/m_validation_group.ts diff --git a/packages/devextreme/js/ui/validation_message.js b/packages/devextreme/js/__internal/ui/m_validation_message.ts similarity index 100% rename from packages/devextreme/js/ui/validation_message.js rename to packages/devextreme/js/__internal/ui/m_validation_message.ts diff --git a/packages/devextreme/js/ui/validation_summary.js b/packages/devextreme/js/__internal/ui/m_validation_summary.ts similarity index 100% rename from packages/devextreme/js/ui/validation_summary.js rename to packages/devextreme/js/__internal/ui/m_validation_summary.ts diff --git a/packages/devextreme/js/ui/validator.js b/packages/devextreme/js/__internal/ui/m_validator.ts similarity index 100% rename from packages/devextreme/js/ui/validator.js rename to packages/devextreme/js/__internal/ui/m_validator.ts diff --git a/packages/devextreme/js/ui/validation/default_adapter.js b/packages/devextreme/js/__internal/ui/validation/m_default_adapter.ts similarity index 100% rename from packages/devextreme/js/ui/validation/default_adapter.js rename to packages/devextreme/js/__internal/ui/validation/m_default_adapter.ts From 68a6b8684813a217afa679cdbf6f913d26fa859c Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 12 Jun 2024 19:03:34 +0400 Subject: [PATCH 29/32] Validator, ValidationMessage, ValidationSummary, ValidationEngine, ValidationGroup: ignore errors after move to TS --- .../js/__internal/ui/m_validation_engine.ts | 1535 +++++++++-------- .../js/__internal/ui/m_validation_group.ts | 106 +- .../js/__internal/ui/m_validation_message.ts | 380 ++-- .../js/__internal/ui/m_validation_summary.ts | 460 ++--- .../js/__internal/ui/m_validator.ts | 502 +++--- .../ui/validation/m_default_adapter.ts | 80 +- .../devextreme/js/ui/validation_engine.js | 5 + packages/devextreme/js/ui/validation_group.js | 26 + .../devextreme/js/ui/validation_message.js | 3 + .../devextreme/js/ui/validation_summary.js | 141 ++ packages/devextreme/js/ui/validator.js | 26 + .../validationGroup.markup.tests.js | 2 +- .../validationGroup.tests.js | 2 +- .../validationSummary.markup.tests.js | 2 +- .../validationSummary.tests.js | 2 +- .../validator.editors.tests.js | 2 +- .../validator.tests.js | 2 +- .../DevExpress.ui.widgets/button.tests.js | 2 +- 18 files changed, 1670 insertions(+), 1608 deletions(-) create mode 100644 packages/devextreme/js/ui/validation_engine.js create mode 100644 packages/devextreme/js/ui/validation_group.js create mode 100644 packages/devextreme/js/ui/validation_message.js create mode 100644 packages/devextreme/js/ui/validation_summary.js create mode 100644 packages/devextreme/js/ui/validator.js diff --git a/packages/devextreme/js/__internal/ui/m_validation_engine.ts b/packages/devextreme/js/__internal/ui/m_validation_engine.ts index 10d0e178fef9..679ee89d2195 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_engine.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_engine.ts @@ -1,857 +1,880 @@ -import Class from '../core/class'; -import { extend } from '../core/utils/extend'; -import { each } from '../core/utils/iterator'; -import { EventsStrategy } from '../core/events_strategy'; -import errors from '../core/errors'; -import { grep } from '../core/utils/common'; +/* eslint-disable max-classes-per-file */ +import Class from '@js/core/class'; +import errors from '@js/core/errors'; +import { EventsStrategy } from '@js/core/events_strategy'; +// @ts-expect-error +import { grep } from '@js/core/utils/common'; import { - isDefined, - isString, - isDate, - isBoolean, - isObject, - isFunction, - isPromise, - isNumeric } from '../core/utils/type'; -import numberLocalization from '../localization/number'; -import messageLocalization from '../localization/message'; -import { fromPromise, Deferred } from '../core/utils/deferred'; + Deferred, + // @ts-expect-error + fromPromise, +} from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { + isBoolean, + isDate, + isDefined, + isFunction, + isNumeric, isObject, + isPromise, + isString, +} from '@js/core/utils/type'; +import messageLocalization from '@js/localization/message'; +import numberLocalization from '@js/localization/number'; const EMAIL_VALIDATION_REGEX = /^[\d\w.+_-]+@[\d\w._-]+\.[\w]+$/i; -// STYLE validation - const STATUS = { - valid: 'valid', - invalid: 'invalid', - pending: 'pending' + valid: 'valid', + invalid: 'invalid', + pending: 'pending', }; class BaseRuleValidator { - constructor() { - this.NAME = 'base'; - } - defaultMessage(value) { - return messageLocalization.getFormatter(`validation-${this.NAME}`)(value); - } - defaultFormattedMessage(value) { - return messageLocalization.getFormatter(`validation-${this.NAME}-formatted`)(value); - } - _isValueEmpty(value) { - return !rulesValidators.required.validate(value, {}); - } - validate(value, rule) { - const valueArray = Array.isArray(value) ? value : [value]; - let result = true; - - if(valueArray.length) { - valueArray.every((itemValue) => { - result = this._validate(itemValue, rule); - return result; - }); - } else { - result = this._validate(null, rule); - } - + public NAME!: string; + + constructor() { + this.NAME = 'base'; + } + + defaultMessage(value) { + // @ts-expect-error + return messageLocalization.getFormatter(`validation-${this.NAME}`)(value); + } + + defaultFormattedMessage(value) { + // @ts-expect-error + return messageLocalization.getFormatter(`validation-${this.NAME}-formatted`)(value); + } + + _isValueEmpty(value) { + return !rulesValidators.required.validate(value, {}); + } + + validate(value, rule) { + const valueArray = Array.isArray(value) ? value : [value]; + let result = true; + + if (valueArray.length) { + valueArray.every((itemValue) => { + // @ts-expect-error + result = this._validate(itemValue, rule); return result; + }); + } else { + // @ts-expect-error + result = this._validate(null, rule); } + + return result; + } } class RequiredRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'required'; + constructor() { + super(); + this.NAME = 'required'; + } + + _validate(value, rule) { + if (!isDefined(value)) return false; + if (value === false) { + return false; } - - _validate(value, rule) { - if(!isDefined(value)) return false; - if(value === false) { - return false; - } - value = String(value); - if(rule.trim || !isDefined(rule.trim)) { - value = value.trim(); - } - return value !== ''; + value = String(value); + if (rule.trim || !isDefined(rule.trim)) { + value = value.trim(); } + return value !== ''; + } } class NumericRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'numeric'; + constructor() { + super(); + this.NAME = 'numeric'; + } + + _validate(value, rule) { + if (rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { + return true; } - - _validate(value, rule) { - if(rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { - return true; - } - if(rule.useCultureSettings && isString(value)) { - return !isNaN(numberLocalization.parse(value)); - } else { - return isNumeric(value); - } + if (rule.useCultureSettings && isString(value)) { + return !isNaN(numberLocalization.parse(value)); } + return isNumeric(value); + } } class RangeRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'range'; + constructor() { + super(); + this.NAME = 'range'; + } + + _validate(value, rule) { + if (rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { + return true; } - - _validate(value, rule) { - if(rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { - return true; - } - const validNumber = rulesValidators['numeric'].validate(value, rule); - const validValue = isDefined(value) && value !== ''; - const number = validNumber ? parseFloat(value) : validValue && value.valueOf(); - const min = rule.min; - const max = rule.max; - if(!(validNumber || isDate(value)) && !validValue) { - return false; - } - if(isDefined(min)) { - if(isDefined(max)) { - return number >= min && number <= max; - } - return number >= min; - } else { - if(isDefined(max)) { - return number <= max; - } else { - throw errors.Error('E0101'); - } - } + const validNumber = rulesValidators.numeric.validate(value, rule); + const validValue = isDefined(value) && value !== ''; + const number = validNumber ? parseFloat(value) : validValue && value.valueOf(); + const { min } = rule; + const { max } = rule; + if (!(validNumber || isDate(value)) && !validValue) { + return false; + } + if (isDefined(min)) { + if (isDefined(max)) { + return number >= min && number <= max; + } + return number >= min; + } + if (isDefined(max)) { + return number <= max; } + throw errors.Error('E0101'); + } } class StringLengthRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'stringLength'; + constructor() { + super(); + this.NAME = 'stringLength'; + } + + _validate(value, rule) { + value = String(value ?? ''); + if (rule.trim || !isDefined(rule.trim)) { + value = value.trim(); } - - _validate(value, rule) { - value = String(value ?? ''); - if(rule.trim || !isDefined(rule.trim)) { - value = value.trim(); - } - if(rule.ignoreEmptyValue && this._isValueEmpty(value)) { - return true; - } - return rulesValidators.range.validate(value.length, - extend({}, rule)); + if (rule.ignoreEmptyValue && this._isValueEmpty(value)) { + return true; } + return rulesValidators.range.validate( + value.length, + extend({}, rule), + ); + } } class CustomRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'custom'; + constructor() { + super(); + this.NAME = 'custom'; + } + + validate(value, rule) { + if (rule.ignoreEmptyValue && this._isValueEmpty(value)) { + return true; } - - validate(value, rule) { - if(rule.ignoreEmptyValue && this._isValueEmpty(value)) { - return true; - } - const validator = rule.validator; - const dataGetter = validator && isFunction(validator.option) && validator.option('dataGetter'); - const extraParams = isFunction(dataGetter) && dataGetter(); - const params = { - value: value, - validator: validator, - rule: rule - }; - if(extraParams) { - extend(params, extraParams); - } - return rule.validationCallback(params); + const { validator } = rule; + const dataGetter = validator && isFunction(validator.option) && validator.option('dataGetter'); + const extraParams = isFunction(dataGetter) && dataGetter(); + const params = { + value, + validator, + rule, + }; + if (extraParams) { + extend(params, extraParams); } + return rule.validationCallback(params); + } } class AsyncRuleValidator extends CustomRuleValidator { - constructor() { - super(); - this.NAME = 'async'; + constructor() { + super(); + this.NAME = 'async'; + } + + validate(value, rule) { + if (!isDefined(rule.reevaluate)) { + extend(rule, { reevaluate: true }); } - - validate(value, rule) { - if(!isDefined(rule.reevaluate)) { - extend(rule, { reevaluate: true }); - } - if(rule.ignoreEmptyValue && this._isValueEmpty(value)) { - return true; - } - const validator = rule.validator; - const dataGetter = validator && isFunction(validator.option) && validator.option('dataGetter'); - const extraParams = isFunction(dataGetter) && dataGetter(); - const params = { - value: value, - validator: validator, - rule: rule - }; - if(extraParams) { - extend(params, extraParams); - } - const callbackResult = rule.validationCallback(params); - if(!isPromise(callbackResult)) { - throw errors.Error('E0103'); - } - return this._getWrappedPromise(fromPromise(callbackResult).promise()); - } - - _getWrappedPromise(promise) { - const deferred = new Deferred(); - promise.then(function(res) { - deferred.resolve(res); - }, function(err) { - const res = { - isValid: false - }; - if(isDefined(err)) { - if(isString(err)) { - res.message = err; - } else if(isObject(err) && isDefined(err.message) && isString(err.message)) { - res.message = err.message; - } - } - deferred.resolve(res); - }); - return deferred.promise(); + if (rule.ignoreEmptyValue && this._isValueEmpty(value)) { + return true; } + const { validator } = rule; + const dataGetter = validator && isFunction(validator.option) && validator.option('dataGetter'); + const extraParams = isFunction(dataGetter) && dataGetter(); + const params = { + value, + validator, + rule, + }; + if (extraParams) { + extend(params, extraParams); + } + const callbackResult = rule.validationCallback(params); + if (!isPromise(callbackResult)) { + throw errors.Error('E0103'); + } + return this._getWrappedPromise(fromPromise(callbackResult).promise()); + } + + _getWrappedPromise(promise) { + const deferred = Deferred(); + promise.then((res) => { + deferred.resolve(res); + }, (err) => { + const res = { + isValid: false, + }; + if (isDefined(err)) { + if (isString(err)) { + // @ts-expect-error + res.message = err; + // @ts-expect-error + } else if (isObject(err) && isDefined(err.message) && isString(err.message)) { + // @ts-expect-error + res.message = err.message; + } + } + deferred.resolve(res); + }); + return deferred.promise(); + } } class CompareRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'compare'; + constructor() { + super(); + this.NAME = 'compare'; + } + + // @ts-expect-error + // eslint-disable-next-line consistent-return + _validate(value, rule) { + if (!rule.comparisonTarget) { + throw errors.Error('E0102'); } - - _validate(value, rule) { - if(!rule.comparisonTarget) { - throw errors.Error('E0102'); - } - if(rule.ignoreEmptyValue && this._isValueEmpty(value)) { - return true; - } - extend(rule, { reevaluate: true }); - const otherValue = rule.comparisonTarget(); - const type = rule.comparisonType || '=='; - switch(type) { - case '==': - return value == otherValue; // eslint-disable-line eqeqeq - case '!=': - return value != otherValue; // eslint-disable-line eqeqeq - case '===': - return value === otherValue; - case '!==': - return value !== otherValue; - case '>': - return value > otherValue; - case '>=': - return value >= otherValue; - case '<': - return value < otherValue; - case '<=': - return value <= otherValue; - } + if (rule.ignoreEmptyValue && this._isValueEmpty(value)) { + return true; } + extend(rule, { reevaluate: true }); + const otherValue = rule.comparisonTarget(); + const type = rule.comparisonType || '=='; + // eslint-disable-next-line default-case + switch (type) { + case '==': + return value == otherValue; // eslint-disable-line eqeqeq + case '!=': + return value != otherValue; // eslint-disable-line eqeqeq + case '===': + return value === otherValue; + case '!==': + return value !== otherValue; + case '>': + return value > otherValue; + case '>=': + return value >= otherValue; + case '<': + return value < otherValue; + case '<=': + return value <= otherValue; + } + } } class PatternRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'pattern'; + constructor() { + super(); + this.NAME = 'pattern'; + } + + _validate(value, rule) { + if (rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { + return true; } - - _validate(value, rule) { - if(rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { - return true; - } - let pattern = rule.pattern; - if(isString(pattern)) { - pattern = new RegExp(pattern); - } - return pattern.test(value); + let { pattern } = rule; + if (isString(pattern)) { + pattern = new RegExp(pattern); } + return pattern.test(value); + } } class EmailRuleValidator extends BaseRuleValidator { - constructor() { - super(); - this.NAME = 'email'; - } - - _validate(value, rule) { - if(rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { - return true; - } - return rulesValidators.pattern.validate(value, - extend({}, - rule, - { - pattern: EMAIL_VALIDATION_REGEX - })); + constructor() { + super(); + this.NAME = 'email'; + } + + _validate(value, rule) { + if (rule.ignoreEmptyValue !== false && this._isValueEmpty(value)) { + return true; } + return rulesValidators.pattern.validate( + value, + extend( + {}, + rule, + { + pattern: EMAIL_VALIDATION_REGEX, + }, + ), + ); + } } const rulesValidators = { - 'required': new RequiredRuleValidator(), + required: new RequiredRuleValidator(), - 'numeric': new NumericRuleValidator(), + numeric: new NumericRuleValidator(), - 'range': new RangeRuleValidator(), + range: new RangeRuleValidator(), - 'stringLength': new StringLengthRuleValidator(), + stringLength: new StringLengthRuleValidator(), - 'custom': new CustomRuleValidator(), + custom: new CustomRuleValidator(), - 'async': new AsyncRuleValidator(), + async: new AsyncRuleValidator(), - 'compare': new CompareRuleValidator(), + compare: new CompareRuleValidator(), - 'pattern': new PatternRuleValidator(), + pattern: new PatternRuleValidator(), - 'email': new EmailRuleValidator() + email: new EmailRuleValidator(), }; const GroupConfig = Class.inherit({ - ctor(group) { - this.group = group; - this.validators = []; - this._pendingValidators = []; - this._onValidatorStatusChanged = this._onValidatorStatusChanged.bind(this); - this._resetValidationInfo(); - this._eventsStrategy = new EventsStrategy(this); - }, - - validate() { - const result = { - isValid: true, - brokenRules: [], - validators: [], - status: STATUS.valid, - complete: null - }; - this._unsubscribeFromAllChangeEvents(); - this._pendingValidators = []; - this._resetValidationInfo(); - each(this.validators, (_, validator) => { - const validatorResult = validator.validate(); - result.isValid = result.isValid && validatorResult.isValid; - if(validatorResult.brokenRules) { - result.brokenRules = result.brokenRules.concat(validatorResult.brokenRules); - } - result.validators.push(validator); - if(validatorResult.status === STATUS.pending) { - this._addPendingValidator(validator); - } - this._subscribeToChangeEvents(validator); - }); - if(this._pendingValidators.length) { - result.status = STATUS.pending; - } else { - result.status = result.isValid ? STATUS.valid : STATUS.invalid; - this._unsubscribeFromAllChangeEvents(); - this._raiseValidatedEvent(result); - } - this._updateValidationInfo(result); - return extend({}, this._validationInfo.result); - }, - - _subscribeToChangeEvents(validator) { - validator.on('validating', this._onValidatorStatusChanged); - validator.on('validated', this._onValidatorStatusChanged); - }, - - _unsubscribeFromChangeEvents(validator) { - validator.off('validating', this._onValidatorStatusChanged); - validator.off('validated', this._onValidatorStatusChanged); - }, - - _unsubscribeFromAllChangeEvents() { - each(this.validators, (_, validator) => { - this._unsubscribeFromChangeEvents(validator); - }); - }, - - _updateValidationInfo(result) { - this._validationInfo.result = result; - if(result.status !== STATUS.pending) { - return; - } - if(!this._validationInfo.deferred) { - this._validationInfo.deferred = new Deferred(); - this._validationInfo.result.complete = this._validationInfo.deferred.promise(); - } - }, - - _addPendingValidator(validator) { - const foundValidator = grep(this._pendingValidators, function(val) { - return val === validator; - })[0]; - if(!foundValidator) { - this._pendingValidators.push(validator); - } - }, - - _removePendingValidator(validator) { - const index = this._pendingValidators.indexOf(validator); - if(index >= 0) { - this._pendingValidators.splice(index, 1); - } - }, - - _orderBrokenRules(brokenRules) { - let orderedRules = []; - each(this.validators, function(_, validator) { - const foundRules = grep(brokenRules, function(rule) { - return rule.validator === validator; - }); - if(foundRules.length) { - orderedRules = orderedRules.concat(foundRules); - } - }); - return orderedRules; - }, + ctor(group) { + this.group = group; + this.validators = []; + this._pendingValidators = []; + this._onValidatorStatusChanged = this._onValidatorStatusChanged.bind(this); + this._resetValidationInfo(); + this._eventsStrategy = new EventsStrategy(this); + }, + + validate() { + const result = { + isValid: true, + brokenRules: [], + validators: [], + status: STATUS.valid, + complete: null, + }; + this._unsubscribeFromAllChangeEvents(); + this._pendingValidators = []; + this._resetValidationInfo(); + each(this.validators, (_, validator) => { + const validatorResult = validator.validate(); + result.isValid = result.isValid && validatorResult.isValid; + if (validatorResult.brokenRules) { + result.brokenRules = result.brokenRules.concat(validatorResult.brokenRules); + } + // @ts-expect-error + result.validators.push(validator); + if (validatorResult.status === STATUS.pending) { + this._addPendingValidator(validator); + } + this._subscribeToChangeEvents(validator); + }); + if (this._pendingValidators.length) { + result.status = STATUS.pending; + } else { + result.status = result.isValid ? STATUS.valid : STATUS.invalid; + this._unsubscribeFromAllChangeEvents(); + this._raiseValidatedEvent(result); + } + this._updateValidationInfo(result); + return extend({}, this._validationInfo.result); + }, + + _subscribeToChangeEvents(validator) { + validator.on('validating', this._onValidatorStatusChanged); + validator.on('validated', this._onValidatorStatusChanged); + }, + + _unsubscribeFromChangeEvents(validator) { + validator.off('validating', this._onValidatorStatusChanged); + validator.off('validated', this._onValidatorStatusChanged); + }, + + _unsubscribeFromAllChangeEvents() { + each(this.validators, (_, validator) => { + this._unsubscribeFromChangeEvents(validator); + }); + }, + + _updateValidationInfo(result) { + this._validationInfo.result = result; + if (result.status !== STATUS.pending) { + return; + } + if (!this._validationInfo.deferred) { + this._validationInfo.deferred = Deferred(); + this._validationInfo.result.complete = this._validationInfo.deferred.promise(); + } + }, - _updateBrokenRules(result) { - if(!this._validationInfo.result) { - return; - } - let brokenRules = this._validationInfo.result.brokenRules; - const rules = grep(brokenRules, function(rule) { - return rule.validator !== result.validator; - }); - if(result.brokenRules) { - brokenRules = rules.concat(result.brokenRules); - } - this._validationInfo.result.brokenRules = this._orderBrokenRules(brokenRules); - }, + _addPendingValidator(validator) { + const foundValidator = grep(this._pendingValidators, (val) => val === validator)[0]; + if (!foundValidator) { + this._pendingValidators.push(validator); + } + }, - _onValidatorStatusChanged(result) { - if(result.status === STATUS.pending) { - this._addPendingValidator(result.validator); - return; - } - this._resolveIfComplete(result); - }, - - _resolveIfComplete(result) { - this._removePendingValidator(result.validator); - this._updateBrokenRules(result); - if(!this._pendingValidators.length) { - this._unsubscribeFromAllChangeEvents(); - if(!this._validationInfo.result) { - return; - } - this._validationInfo.result.status = this._validationInfo.result.brokenRules.length === 0 ? STATUS.valid : STATUS.invalid; - this._validationInfo.result.isValid = this._validationInfo.result.status === STATUS.valid; - const res = extend({}, this._validationInfo.result, { complete: null }); - const deferred = this._validationInfo.deferred; - this._validationInfo.deferred = null; - this._raiseValidatedEvent(res); - deferred && setTimeout(() => { - deferred.resolve(res); - }); - } - }, - - _raiseValidatedEvent(result) { - this._eventsStrategy.fireEvent('validated', [result]); - }, - - _resetValidationInfo() { - this._validationInfo = { - result: null, - deferred: null - }; - }, - - _synchronizeValidationInfo() { - if(this._validationInfo.result) { - this._validationInfo.result.validators = this.validators; - } - }, - - removeRegisteredValidator(validator) { - const index = this.validators.indexOf(validator); - if(index > -1) { - this.validators.splice(index, 1); - this._synchronizeValidationInfo(); - this._resolveIfComplete({ validator }); - } - }, + _removePendingValidator(validator) { + const index = this._pendingValidators.indexOf(validator); + if (index >= 0) { + this._pendingValidators.splice(index, 1); + } + }, + + _orderBrokenRules(brokenRules) { + let orderedRules = []; + each(this.validators, (_, validator) => { + const foundRules = grep(brokenRules, (rule) => rule.validator === validator); + if (foundRules.length) { + orderedRules = orderedRules.concat(foundRules); + } + }); + return orderedRules; + }, + + _updateBrokenRules(result) { + if (!this._validationInfo.result) { + return; + } + let { brokenRules } = this._validationInfo.result; + const rules = grep(brokenRules, (rule) => rule.validator !== result.validator); + if (result.brokenRules) { + brokenRules = rules.concat(result.brokenRules); + } + this._validationInfo.result.brokenRules = this._orderBrokenRules(brokenRules); + }, - registerValidator(validator) { - if(!this.validators.includes(validator)) { - this.validators.push(validator); - this._synchronizeValidationInfo(); - } - }, + _onValidatorStatusChanged(result) { + if (result.status === STATUS.pending) { + this._addPendingValidator(result.validator); + return; + } + this._resolveIfComplete(result); + }, + + _resolveIfComplete(result) { + this._removePendingValidator(result.validator); + this._updateBrokenRules(result); + if (!this._pendingValidators.length) { + this._unsubscribeFromAllChangeEvents(); + if (!this._validationInfo.result) { + return; + } + this._validationInfo.result.status = this._validationInfo.result.brokenRules.length === 0 ? STATUS.valid : STATUS.invalid; + this._validationInfo.result.isValid = this._validationInfo.result.status === STATUS.valid; + const res = extend({}, this._validationInfo.result, { complete: null }); + const { deferred } = this._validationInfo; + this._validationInfo.deferred = null; + this._raiseValidatedEvent(res); + deferred && setTimeout(() => { + deferred.resolve(res); + }); + } + }, + + _raiseValidatedEvent(result) { + this._eventsStrategy.fireEvent('validated', [result]); + }, + + _resetValidationInfo() { + this._validationInfo = { + result: null, + deferred: null, + }; + }, + + _synchronizeValidationInfo() { + if (this._validationInfo.result) { + this._validationInfo.result.validators = this.validators; + } + }, + + removeRegisteredValidator(validator) { + const index = this.validators.indexOf(validator); + if (index > -1) { + this.validators.splice(index, 1); + this._synchronizeValidationInfo(); + this._resolveIfComplete({ validator }); + } + }, - reset() { - each(this.validators, function(_, validator) { - validator.reset(); - }); - this._pendingValidators = []; - this._resetValidationInfo(); - }, - - on(eventName, eventHandler) { - this._eventsStrategy.on(eventName, eventHandler); - return this; - }, - - off(eventName, eventHandler) { - this._eventsStrategy.off(eventName, eventHandler); - return this; - }, + registerValidator(validator) { + if (!this.validators.includes(validator)) { + this.validators.push(validator); + this._synchronizeValidationInfo(); + } + }, + + reset() { + each(this.validators, (_, validator) => { + validator.reset(); + }); + this._pendingValidators = []; + this._resetValidationInfo(); + }, + + on(eventName, eventHandler) { + this._eventsStrategy.on(eventName, eventHandler); + return this; + }, + + off(eventName, eventHandler) { + this._eventsStrategy.off(eventName, eventHandler); + return this; + }, }); const ValidationEngine = { - groups: [], + groups: [], - getGroupConfig(group) { - const result = grep(this.groups, function(config) { - return config.group === group; - }); - if(result.length) { - return result[0]; - } - }, + getGroupConfig(group) { + const result = grep(this.groups, (config) => config.group === group); + if (result.length) { + return result[0]; + } + }, - findGroup($element, model) { - const hasValidationGroup = $element.data()?.dxComponents?.includes('dxValidationGroup'); - const validationGroup = hasValidationGroup && $element.dxValidationGroup('instance'); + findGroup($element, model) { + const hasValidationGroup = $element.data()?.dxComponents?.includes('dxValidationGroup'); + const validationGroup = hasValidationGroup && $element.dxValidationGroup('instance'); - if(validationGroup) { - return validationGroup; - } - // try to find out if this control is child of validation group - const $dxGroup = $element.parents('.dx-validationgroup').first(); + if (validationGroup) { + return validationGroup; + } + // try to find out if this control is child of validation group + const $dxGroup = $element.parents('.dx-validationgroup').first(); - if($dxGroup.length) { - return $dxGroup.dxValidationGroup('instance'); - } + if ($dxGroup.length) { + return $dxGroup.dxValidationGroup('instance'); + } - // Trick to be able to securely get ViewModel instance ($data) in Knockout - return model; - }, + // Trick to be able to securely get ViewModel instance ($data) in Knockout + return model; + }, - initGroups() { - this.groups = []; - this.addGroup(); - }, + initGroups() { + this.groups = []; + this.addGroup(); + }, - addGroup(group) { - let config = this.getGroupConfig(group); - if(!config) { - config = new GroupConfig(group); - this.groups.push(config); - } - return config; - }, - - removeGroup(group) { - const config = this.getGroupConfig(group); - const index = this.groups.indexOf(config); - if(index > -1) { - this.groups.splice(index, 1); - } - return config; - }, - - _setDefaultMessage(info) { - const { rule, validator, name } = info; - if(!isDefined(rule.message)) { - if(validator.defaultFormattedMessage && isDefined(name)) { - rule.message = validator.defaultFormattedMessage(name); - } else { - rule.message = validator.defaultMessage(); - } - } - }, + addGroup(group) { + let config = this.getGroupConfig(group); + if (!config) { + config = new GroupConfig(group); + this.groups.push(config); + } + return config; + }, + + removeGroup(group) { + const config = this.getGroupConfig(group); + const index = this.groups.indexOf(config); + if (index > -1) { + this.groups.splice(index, 1); + } + return config; + }, + + _setDefaultMessage(info) { + const { rule, validator, name } = info; + if (!isDefined(rule.message)) { + if (validator.defaultFormattedMessage && isDefined(name)) { + rule.message = validator.defaultFormattedMessage(name); + } else { + rule.message = validator.defaultMessage(); + } + } + }, - _addBrokenRule(info) { - const { result, rule } = info; - if(!result.brokenRule) { - result.brokenRule = rule; - } - if(!result.brokenRules) { - result.brokenRules = []; - } - result.brokenRules.push(rule); - }, - - validate(value, rules, name) { - let result = { - name: name, - value: value, - brokenRule: null, - brokenRules: null, - isValid: true, - validationRules: rules, - pendingRules: null, - status: STATUS.valid, - complete: null - }; - const validator = rules?.[0]?.validator; - - const asyncRuleItems = []; - each(rules || [], (_, rule) => { - const ruleValidator = rulesValidators[rule.type]; - let ruleValidationResult; - if(ruleValidator) { - if(isDefined(rule.isValid) && rule.value === value && !rule.reevaluate) { - if(!rule.isValid) { - result.isValid = false; - this._addBrokenRule({ - result, - rule - }); - return false; - } - return true; - } - rule.value = value; - if(rule.type === 'async') { - asyncRuleItems.push({ - rule: rule, - ruleValidator: ruleValidator - }); - return true; - } - ruleValidationResult = ruleValidator.validate(value, rule); - rule.isValid = ruleValidationResult; - if(!ruleValidationResult) { - result.isValid = false; - this._setDefaultMessage({ - rule, - validator: ruleValidator, - name - }); - this._addBrokenRule({ - result, - rule - }); - } - if(!rule.isValid) { - return false; - } - } else { - throw errors.Error('E0100'); - } - }); - if(result.isValid && !result.brokenRules && asyncRuleItems.length) { - result = this._validateAsyncRules({ - value, - items: asyncRuleItems, - result, - name + _addBrokenRule(info) { + const { result, rule } = info; + if (!result.brokenRule) { + result.brokenRule = rule; + } + if (!result.brokenRules) { + result.brokenRules = []; + } + result.brokenRules.push(rule); + }, + + validate(value, rules, name) { + let result = { + name, + value, + brokenRule: null, + brokenRules: null, + isValid: true, + validationRules: rules, + pendingRules: null, + status: STATUS.valid, + complete: null, + }; + const validator = rules?.[0]?.validator; + + const asyncRuleItems = []; + // @ts-expect-error + each(rules || [], (_, rule) => { + const ruleValidator = rulesValidators[rule.type]; + let ruleValidationResult; + if (ruleValidator) { + if (isDefined(rule.isValid) && rule.value === value && !rule.reevaluate) { + if (!rule.isValid) { + result.isValid = false; + this._addBrokenRule({ + result, + rule, }); - } + return false; + } + return true; + } + rule.value = value; + if (rule.type === 'async') { + // @ts-expect-error + asyncRuleItems.push({ + rule, + ruleValidator, + }); + return true; + } + ruleValidationResult = ruleValidator.validate(value, rule); + rule.isValid = ruleValidationResult; + if (!ruleValidationResult) { + result.isValid = false; + this._setDefaultMessage({ + rule, + validator: ruleValidator, + name, + }); + this._addBrokenRule({ + result, + rule, + }); + } + if (!rule.isValid) { + return false; + } + } else { + throw errors.Error('E0100'); + } + }); + if (result.isValid && !result.brokenRules && asyncRuleItems.length) { + result = this._validateAsyncRules({ + value, + items: asyncRuleItems, + result, + name, + }); + } - this._synchronizeGroupValidationInfo(validator, result); + this._synchronizeGroupValidationInfo(validator, result); - result.status = result.pendingRules ? STATUS.pending : (result.isValid ? STATUS.valid : STATUS.invalid); - return result; - }, + result.status = result.pendingRules ? STATUS.pending : result.isValid ? STATUS.valid : STATUS.invalid; + return result; + }, - _synchronizeGroupValidationInfo(validator, result) { - if(!validator) { - return; - } - const groupConfig = ValidationEngine.getGroupConfig(validator._validationGroup); - groupConfig._updateBrokenRules.call(groupConfig, { validator, brokenRules: result.brokenRules ?? [] }); - }, - - _validateAsyncRules({ result, value, items, name }) { - const asyncResults = []; - each(items, (_, item) => { - const validateResult = item.ruleValidator.validate(value, item.rule); - if(!isPromise(validateResult)) { - this._updateRuleConfig({ - rule: item.rule, - ruleResult: this._getPatchedRuleResult(validateResult), - validator: item.ruleValidator, - name - }); - } else { - if(!result.pendingRules) { - result.pendingRules = []; - } - result.pendingRules.push(item.rule); - const asyncResult = validateResult.then((res) => { - const ruleResult = this._getPatchedRuleResult(res); - this._updateRuleConfig({ - rule: item.rule, - ruleResult, - validator: item.ruleValidator, - name - }); - return ruleResult; - }); - asyncResults.push(asyncResult); - } + _synchronizeGroupValidationInfo(validator, result) { + if (!validator) { + return; + } + const groupConfig = ValidationEngine.getGroupConfig(validator._validationGroup); + groupConfig._updateBrokenRules.call(groupConfig, { validator, brokenRules: result.brokenRules ?? [] }); + }, + + _validateAsyncRules({ + result, value, items, name, + }) { + const asyncResults = []; + each(items, (_, item) => { + const validateResult = item.ruleValidator.validate(value, item.rule); + if (!isPromise(validateResult)) { + this._updateRuleConfig({ + rule: item.rule, + ruleResult: this._getPatchedRuleResult(validateResult), + validator: item.ruleValidator, + name, }); - if(asyncResults.length) { - result.complete = Promise.all(asyncResults).then((values) => { - return this._getAsyncRulesResult({ - result, - values - }); - }); - } - return result; - }, - - _updateRuleConfig({ rule, ruleResult, validator, name }) { - rule.isValid = ruleResult.isValid; - if(!ruleResult.isValid) { - if(isDefined(ruleResult.message) && isString(ruleResult.message) && ruleResult.message.length) { - rule.message = ruleResult.message; - } else { - this._setDefaultMessage({ - rule, - validator: validator, - name - }); - } - } - }, - - _getPatchedRuleResult(ruleResult) { - let result; - const isValid = true; - if(isObject(ruleResult)) { - result = extend({}, ruleResult); - if(!isDefined(result.isValid)) { - result.isValid = isValid; - } - } else { - result = { - isValid: isBoolean(ruleResult) ? ruleResult : isValid - }; - } - return result; - }, - - _getAsyncRulesResult({ values, result }) { - each(values, (index, val) => { - if(val.isValid === false) { - result.isValid = val.isValid; - const rule = result.pendingRules[index]; - this._addBrokenRule({ - result, - rule - }); - } + } else { + if (!result.pendingRules) { + result.pendingRules = []; + } + result.pendingRules.push(item.rule); + const asyncResult = validateResult.then((res) => { + const ruleResult = this._getPatchedRuleResult(res); + this._updateRuleConfig({ + rule: item.rule, + ruleResult, + validator: item.ruleValidator, + name, + }); + return ruleResult; }); - result.pendingRules = null; - result.complete = null; - result.status = result.isValid ? STATUS.valid : STATUS.invalid; - return result; - }, - - registerValidatorInGroup(group, validator) { - const groupConfig = ValidationEngine.addGroup(group); - groupConfig.registerValidator.call(groupConfig, validator); - }, - - _shouldRemoveGroup(group, validatorsInGroup) { - const isDefaultGroup = group === undefined; - const isValidationGroupInstance = group && group.NAME === 'dxValidationGroup'; - return !isDefaultGroup && !isValidationGroupInstance && !validatorsInGroup.length; - }, - - removeRegisteredValidator(group, validator) { - const config = ValidationEngine.getGroupConfig(group); - if(config) { - config.removeRegisteredValidator.call(config, validator); - const validatorsInGroup = config.validators; - if(this._shouldRemoveGroup(group, validatorsInGroup)) { - this.removeGroup(group); - } - } - }, + // @ts-expect-error + asyncResults.push(asyncResult); + } + }); + if (asyncResults.length) { + result.complete = Promise.all(asyncResults).then((values) => this._getAsyncRulesResult({ + result, + values, + })); + } + return result; + }, + + _updateRuleConfig({ + rule, ruleResult, validator, name, + }) { + rule.isValid = ruleResult.isValid; + if (!ruleResult.isValid) { + if (isDefined(ruleResult.message) && isString(ruleResult.message) && ruleResult.message.length) { + rule.message = ruleResult.message; + } else { + this._setDefaultMessage({ + rule, + validator, + name, + }); + } + } + }, + + _getPatchedRuleResult(ruleResult) { + let result; + const isValid = true; + if (isObject(ruleResult)) { + result = extend({}, ruleResult); + if (!isDefined(result.isValid)) { + result.isValid = isValid; + } + } else { + result = { + isValid: isBoolean(ruleResult) ? ruleResult : isValid, + }; + } + return result; + }, + + _getAsyncRulesResult({ values, result }) { + each(values, (index, val) => { + if (val.isValid === false) { + result.isValid = val.isValid; + const rule = result.pendingRules[index]; + this._addBrokenRule({ + result, + rule, + }); + } + }); + result.pendingRules = null; + result.complete = null; + result.status = result.isValid ? STATUS.valid : STATUS.invalid; + return result; + }, + + registerValidatorInGroup(group, validator) { + const groupConfig = ValidationEngine.addGroup(group); + groupConfig.registerValidator.call(groupConfig, validator); + }, + + _shouldRemoveGroup(group, validatorsInGroup) { + const isDefaultGroup = group === undefined; + const isValidationGroupInstance = group && group.NAME === 'dxValidationGroup'; + return !isDefaultGroup && !isValidationGroupInstance && !validatorsInGroup.length; + }, + + removeRegisteredValidator(group, validator) { + const config = ValidationEngine.getGroupConfig(group); + if (config) { + config.removeRegisteredValidator.call(config, validator); + const validatorsInGroup = config.validators; + if (this._shouldRemoveGroup(group, validatorsInGroup)) { + this.removeGroup(group); + } + } + }, - initValidationOptions(options) { - const initedOptions = {}; + initValidationOptions(options) { + const initedOptions = {}; - if(options) { - const syncOptions = ['isValid', 'validationStatus', 'validationError', 'validationErrors']; + if (options) { + const syncOptions = ['isValid', 'validationStatus', 'validationError', 'validationErrors']; - syncOptions.forEach((prop) => { - if(prop in options) { - extend(initedOptions, - this.synchronizeValidationOptions({ name: prop, value: options[prop] }, options) - ); - } - }); + syncOptions.forEach((prop) => { + if (prop in options) { + extend( + initedOptions, + this.synchronizeValidationOptions({ name: prop, value: options[prop] }, options), + ); } + }); + } - return initedOptions; - }, - - synchronizeValidationOptions({ name, value }, options) { - switch(name) { - case 'validationStatus': { - const isValid = value === STATUS.valid || value === STATUS.pending; - - return options.isValid !== isValid ? { isValid } : {}; - } - case 'isValid': { - const { validationStatus } = options; - let newStatus = validationStatus; - - if(value && validationStatus === STATUS.invalid) { - newStatus = STATUS.valid; - } else if(!value && validationStatus !== STATUS.invalid) { - newStatus = STATUS.invalid; - } - - return newStatus !== validationStatus ? { validationStatus: newStatus } : {}; - } - case 'validationErrors': { - const validationError = !value || !value.length ? null : value[0]; - - return options.validationError !== validationError ? { validationError } : {}; - } - case 'validationError': { - const { validationErrors } = options; - - if(!value && validationErrors) { - return { validationErrors: null }; - } else if(value && !validationErrors) { - return { validationErrors: [value] }; - } else if(value && validationErrors && value !== validationErrors[0]) { - validationErrors[0] = value; - return { validationErrors: validationErrors.slice() }; - } - } - } + return initedOptions; + }, + + synchronizeValidationOptions({ name, value }, options) { + // eslint-disable-next-line default-case + switch (name) { + case 'validationStatus': { + const isValid = value === STATUS.valid || value === STATUS.pending; + + return options.isValid !== isValid ? { isValid } : {}; + } + case 'isValid': { + const { validationStatus } = options; + let newStatus = validationStatus; + + if (value && validationStatus === STATUS.invalid) { + newStatus = STATUS.valid; + } else if (!value && validationStatus !== STATUS.invalid) { + newStatus = STATUS.invalid; + } + + return newStatus !== validationStatus ? { validationStatus: newStatus } : {}; + } + case 'validationErrors': { + const validationError = !value || !value.length ? null : value[0]; + + return options.validationError !== validationError ? { validationError } : {}; + } + case 'validationError': { + const { validationErrors } = options; + + if (!value && validationErrors) { + return { validationErrors: null }; + } if (value && !validationErrors) { + return { validationErrors: [value] }; + } if (value && validationErrors && value !== validationErrors[0]) { + validationErrors[0] = value; + return { validationErrors: validationErrors.slice() }; + } + } + } - return {}; - }, + return {}; + }, - validateGroup(group) { - const groupConfig = ValidationEngine.getGroupConfig(group); - if(!groupConfig) { - throw errors.Error('E0110'); - } - return groupConfig.validate(); - }, + validateGroup(group) { + const groupConfig = ValidationEngine.getGroupConfig(group); + if (!groupConfig) { + throw errors.Error('E0110'); + } + return groupConfig.validate(); + }, - resetGroup(group) { - const groupConfig = ValidationEngine.getGroupConfig(group); - if(!groupConfig) { - throw errors.Error('E0110'); - } - return groupConfig.reset(); + resetGroup(group) { + const groupConfig = ValidationEngine.getGroupConfig(group); + if (!groupConfig) { + throw errors.Error('E0110'); } + return groupConfig.reset(); + }, }; ValidationEngine.initGroups(); diff --git a/packages/devextreme/js/__internal/ui/m_validation_group.ts b/packages/devextreme/js/__internal/ui/m_validation_group.ts index 70635c70bd45..9bd94e40c99f 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_group.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_group.ts @@ -1,74 +1,62 @@ -import $ from '../core/renderer'; -import registerComponent from '../core/component_registrator'; -import DOMComponent from '../core/dom_component'; -import ValidationSummary from './validation_summary'; -import ValidationEngine from './validation_engine'; -import Validator from './validator'; +import registerComponent from '@js/core/component_registrator'; +import DOMComponent from '@js/core/dom_component'; +import $ from '@js/core/renderer'; + +import ValidationEngine from './m_validation_engine'; +import ValidationSummary from './m_validation_summary'; +import Validator from './m_validator'; const VALIDATION_ENGINE_CLASS = 'dx-validationgroup'; const VALIDATOR_CLASS = 'dx-validator'; const VALIDATION_SUMMARY_CLASS = 'dx-validationsummary'; class ValidationGroup extends DOMComponent { - _getDefaultOptions() { - return super._getDefaultOptions(); - - /** - * @name dxValidationGroupOptions.rtlEnabled - * @hidden - */ - - /** - * @name dxValidationGroup.beginUpdate - * @publicName beginUpdate() - * @hidden - */ - /** - * @name dxValidationGroup.defaultOptions - * @publicName defaultOptions(rule) - * @hidden - */ - /** - * @name dxValidationGroup.endUpdate - * @publicName endUpdate() - * @hidden - */ - } + _getDefaultOptions() { + // @ts-expect-error + return super._getDefaultOptions(); + } - _init() { - super._init(); - ValidationEngine.addGroup(this); - } + _init() { + // @ts-expect-error + super._init(); + ValidationEngine.addGroup(this); + } - _initMarkup() { - const $element = this.$element(); - $element.addClass(VALIDATION_ENGINE_CLASS); - $element.find(`.${VALIDATOR_CLASS}`).each(function(_, validatorContainer) { - Validator.getInstance($(validatorContainer))._initGroupRegistration(); - }); - $element.find(`.${VALIDATION_SUMMARY_CLASS}`).each(function(_, summaryContainer) { - ValidationSummary.getInstance($(summaryContainer)).refreshValidationGroup(); - }); - super._initMarkup(); - } + _initMarkup() { + const $element = this.$element(); + // @ts-expect-error + $element.addClass(VALIDATION_ENGINE_CLASS); + // @ts-expect-error + $element.find(`.${VALIDATOR_CLASS}`).each((_, validatorContainer) => { + Validator.getInstance($(validatorContainer))._initGroupRegistration(); + }); + // @ts-expect-error + $element.find(`.${VALIDATION_SUMMARY_CLASS}`).each((_, summaryContainer) => { + ValidationSummary.getInstance($(summaryContainer)).refreshValidationGroup(); + }); + // @ts-expect-error + super._initMarkup(); + } - validate() { - return ValidationEngine.validateGroup(this); - } + validate() { + return ValidationEngine.validateGroup(this); + } - reset() { - return ValidationEngine.resetGroup(this); - } + reset() { + return ValidationEngine.resetGroup(this); + } - _dispose() { - ValidationEngine.removeGroup(this); - this.$element().removeClass(VALIDATION_ENGINE_CLASS); - super._dispose(); - } + _dispose() { + ValidationEngine.removeGroup(this); + // @ts-expect-error + this.$element().removeClass(VALIDATION_ENGINE_CLASS); + // @ts-expect-error + super._dispose(); + } - _useTemplates() { - return false; - } + _useTemplates() { + return false; + } } registerComponent('dxValidationGroup', ValidationGroup); diff --git a/packages/devextreme/js/__internal/ui/m_validation_message.ts b/packages/devextreme/js/__internal/ui/m_validation_message.ts index bfafd004f23f..6bc9c0a8dffc 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_message.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_message.ts @@ -1,203 +1,203 @@ -import { getOuterWidth } from '../core/utils/size'; -import $ from '../core/renderer'; -import registerComponent from '../core/component_registrator'; -import Overlay from './overlay/ui.overlay'; -import { extend } from '../core/utils/extend'; -import { encodeHtml } from '../core/utils/string'; -import { getDefaultAlignment } from '../core/utils/position'; +import registerComponent from '@js/core/component_registrator'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { getDefaultAlignment } from '@js/core/utils/position'; +import { getOuterWidth } from '@js/core/utils/size'; +import { encodeHtml } from '@js/core/utils/string'; +import Overlay from '@js/ui/overlay/ui.overlay'; const INVALID_MESSAGE = 'dx-invalid-message'; const INVALID_MESSAGE_AUTO = 'dx-invalid-message-auto'; const INVALID_MESSAGE_ALWAYS = 'dx-invalid-message-always'; const INVALID_MESSAGE_CONTENT = 'dx-invalid-message-content'; const VALIDATION_MESSAGE_MIN_WIDTH = 100; - +// @ts-expect-error const ValidationMessage = Overlay.inherit({ - _getDefaultOptions() { - return extend(this.callBase(), { - integrationOptions: {}, - templatesRenderAsynchronously: false, - shading: false, - width: 'auto', - height: 'auto', - hideOnOutsideClick: false, - animation: null, - visible: true, - propagateOutsideClick: true, - _checkParentVisibility: false, - rtlEnabled: false, - contentTemplate: this._renderInnerHtml, - maxWidth: '100%', - - container: this.$element(), - target: undefined, - mode: 'auto', - validationErrors: undefined, - preventScrollEvents: false, - positionSide: 'top', - boundary: undefined, - offset: { h: 0, v: 0 }, - contentId: undefined - }); - }, - - _init() { - this.callBase(); + _getDefaultOptions() { + return extend(this.callBase(), { + integrationOptions: {}, + templatesRenderAsynchronously: false, + shading: false, + width: 'auto', + height: 'auto', + hideOnOutsideClick: false, + animation: null, + visible: true, + propagateOutsideClick: true, + _checkParentVisibility: false, + rtlEnabled: false, + contentTemplate: this._renderInnerHtml, + maxWidth: '100%', + + container: this.$element(), + target: undefined, + mode: 'auto', + validationErrors: undefined, + preventScrollEvents: false, + positionSide: 'top', + boundary: undefined, + offset: { h: 0, v: 0 }, + contentId: undefined, + }); + }, + + _init() { + this.callBase(); + this.updateMaxWidth(); + this._updatePosition(); + }, + + _initMarkup() { + this.callBase(); + + this._ensureMessageNotEmpty(); + this._updatePositionByTarget(); + this._toggleModeClass(); + this._updateContentId(); + }, + + _updatePositionByTarget() { + const { target } = this.option(); + + this.option('position.of', target); + }, + + _ensureMessageNotEmpty() { + this._textMarkup = this._getTextMarkup(); + + const shouldShowMessage = this.option('visible') && this._textMarkup; + this._toggleVisibilityClasses(shouldShowMessage); + }, + + _toggleVisibilityClasses(visible) { + if (visible) { + this.$element().addClass(INVALID_MESSAGE); + this.$wrapper().addClass(INVALID_MESSAGE); + } else { + this.$element().removeClass(INVALID_MESSAGE); + this.$wrapper().removeClass(INVALID_MESSAGE); + } + }, + + _updateContentId() { + const { container, contentId } = this.option(); + const id = contentId ?? $(container).attr('aria-describedby'); + + this.$content() + .addClass(INVALID_MESSAGE_CONTENT) + .attr('id', id); + }, + + _renderInnerHtml(element) { + const $element = element && $(element); + + $element?.html(this._textMarkup); + }, + + _getTextMarkup() { + const validationErrors = this.option('validationErrors') ?? []; + let validationErrorMessage = ''; + validationErrors.forEach((err) => { + const separator = validationErrorMessage ? '
' : ''; + validationErrorMessage += separator + encodeHtml(err?.message ?? ''); + }); + + return validationErrorMessage; + }, + + _toggleModeClass() { + const mode = this.option('mode'); + this.$wrapper() + .toggleClass(INVALID_MESSAGE_AUTO, mode === 'auto') + .toggleClass(INVALID_MESSAGE_ALWAYS, mode === 'always'); + }, + + updateMaxWidth() { + const target = this.option('target'); + const targetWidth = getOuterWidth(target); + let maxWidth = '100%'; + if (targetWidth) { + // @ts-expect-error + maxWidth = Math.max(targetWidth, VALIDATION_MESSAGE_MIN_WIDTH); + } + + this.option({ maxWidth }); + }, + + _getPositionsArray(positionSide, rtlSide) { + switch (positionSide) { + case 'top': + return [`${rtlSide} bottom`, `${rtlSide} top`]; + case 'left': + return ['right', 'left']; + case 'right': + return ['left', 'right']; + default: + return [`${rtlSide} top`, `${rtlSide} bottom`]; + } + }, + + _updatePosition() { + const { + positionSide, + rtlEnabled, + offset: componentOffset, + boundary, + } = this.option(); + const rtlSide = getDefaultAlignment(rtlEnabled); + const positions = this._getPositionsArray(positionSide, rtlSide); + const offset = { ...componentOffset }; + + this.$element().addClass(`dx-invalid-message-${positionSide}`); + + if (rtlEnabled && positionSide !== 'left' && positionSide !== 'right') offset.h = -offset.h; + if (positionSide === 'top') offset.v = -offset.v; + if (positionSide === 'left') offset.h = -offset.h; + + this.option('position', { + offset, + boundary, + my: positions[0], + at: positions[1], + collision: 'none flip', + }); + }, + + _optionChanged(args) { + const { name, value, previousValue } = args; + switch (name) { + case 'target': + this._updatePositionByTarget(); this.updateMaxWidth(); + this.callBase(args); + break; + case 'boundary': + this.option('position.boundary', value); + break; + case 'mode': + this._toggleModeClass(value); + break; + case 'rtlEnabled': + case 'offset': + case 'positionSide': + this.$element().removeClass(`dx-invalid-message-${previousValue}`); this._updatePosition(); - }, - - _initMarkup() { - this.callBase(); - - this._ensureMessageNotEmpty(); - this._updatePositionByTarget(); - this._toggleModeClass(); + break; + case 'container': this._updateContentId(); - }, - - _updatePositionByTarget: function() { - const { target } = this.option(); - - this.option('position.of', target); - }, - - _ensureMessageNotEmpty: function() { - this._textMarkup = this._getTextMarkup(); - - const shouldShowMessage = this.option('visible') && this._textMarkup; - this._toggleVisibilityClasses(shouldShowMessage); - }, - - _toggleVisibilityClasses: function(visible) { - if(visible) { - this.$element().addClass(INVALID_MESSAGE); - this.$wrapper().addClass(INVALID_MESSAGE); - } else { - this.$element().removeClass(INVALID_MESSAGE); - this.$wrapper().removeClass(INVALID_MESSAGE); - } - }, - - _updateContentId() { - const { container, contentId } = this.option(); - const id = contentId ?? $(container).attr('aria-describedby'); - - this.$content() - .addClass(INVALID_MESSAGE_CONTENT) - .attr('id', id); - }, - - _renderInnerHtml(element) { - const $element = element && $(element); - - $element?.html(this._textMarkup); - }, - - _getTextMarkup() { - const validationErrors = this.option('validationErrors') ?? []; - let validationErrorMessage = ''; - validationErrors.forEach((err) => { - const separator = validationErrorMessage ? '
' : ''; - validationErrorMessage += separator + encodeHtml(err?.message ?? ''); - }); - - return validationErrorMessage; - }, - - _toggleModeClass() { - const mode = this.option('mode'); - this.$wrapper() - .toggleClass(INVALID_MESSAGE_AUTO, mode === 'auto') - .toggleClass(INVALID_MESSAGE_ALWAYS, mode === 'always'); - }, - - updateMaxWidth() { - const target = this.option('target'); - const targetWidth = getOuterWidth(target); - let maxWidth = '100%'; - if(targetWidth) { - maxWidth = Math.max(targetWidth, VALIDATION_MESSAGE_MIN_WIDTH); - } - - this.option({ maxWidth }); - }, - - _getPositionsArray: function(positionSide, rtlSide) { - switch(positionSide) { - case 'top': - return [`${rtlSide} bottom`, `${rtlSide} top`]; - case 'left': - return ['right', 'left']; - case 'right': - return ['left', 'right']; - default: - return [`${rtlSide} top`, `${rtlSide} bottom`]; - } - - }, - - _updatePosition: function() { - const { - positionSide, - rtlEnabled, - offset: componentOffset, - boundary - } = this.option(); - const rtlSide = getDefaultAlignment(rtlEnabled); - const positions = this._getPositionsArray(positionSide, rtlSide); - const offset = Object.assign({}, componentOffset); - - this.$element().addClass(`dx-invalid-message-${positionSide}`); - - if(rtlEnabled && positionSide !== 'left' && positionSide !== 'right') offset.h = -offset.h; - if(positionSide === 'top') offset.v = -offset.v; - if(positionSide === 'left') offset.h = -offset.h; - - this.option('position', { - offset, - boundary, - my: positions[0], - at: positions[1], - collision: 'none flip' - }); - }, - - _optionChanged(args) { - const { name, value, previousValue } = args; - switch(name) { - case 'target': - this._updatePositionByTarget(); - this.updateMaxWidth(); - this.callBase(args); - break; - case 'boundary': - this.option('position.boundary', value); - break; - case 'mode': - this._toggleModeClass(value); - break; - case 'rtlEnabled': - case 'offset': - case 'positionSide': - this.$element().removeClass(`dx-invalid-message-${previousValue}`); - this._updatePosition(); - break; - case 'container': - this._updateContentId(); - this.callBase(args); - break; - case 'contentId': - this._updateContentId(); - break; - case 'validationErrors': - this._ensureMessageNotEmpty(); - this._renderInnerHtml(this.$content()); - break; - default: - this.callBase(args); - } - }, + this.callBase(args); + break; + case 'contentId': + this._updateContentId(); + break; + case 'validationErrors': + this._ensureMessageNotEmpty(); + this._renderInnerHtml(this.$content()); + break; + default: + this.callBase(args); + } + }, }); registerComponent('dxValidationMessage', ValidationMessage); diff --git a/packages/devextreme/js/__internal/ui/m_validation_summary.ts b/packages/devextreme/js/__internal/ui/m_validation_summary.ts index fbd830e47d51..13c7a2284305 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_summary.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_summary.ts @@ -1,311 +1,179 @@ -import registerComponent from '../core/component_registrator'; -import eventsEngine from '../events/core/events_engine'; -import { grep } from '../core/utils/common'; -import { extend } from '../core/utils/extend'; -import { each, map } from '../core/utils/iterator'; -import ValidationEngine from './validation_engine'; -import CollectionWidget from './collection/ui.collection_widget.edit'; +import registerComponent from '@js/core/component_registrator'; +// @ts-expect-error +import { grep } from '@js/core/utils/common'; +import { extend } from '@js/core/utils/extend'; +import { each, map } from '@js/core/utils/iterator'; +import eventsEngine from '@js/events/core/events_engine'; +import CollectionWidget from '@js/ui/collection/ui.collection_widget.edit'; + +import ValidationEngine from './m_validation_engine'; const VALIDATION_SUMMARY_CLASS = 'dx-validationsummary'; -const ITEM_CLASS = VALIDATION_SUMMARY_CLASS + '-item'; -const ITEM_DATA_KEY = VALIDATION_SUMMARY_CLASS + '-item-data'; +const ITEM_CLASS = `${VALIDATION_SUMMARY_CLASS}-item`; +const ITEM_DATA_KEY = `${VALIDATION_SUMMARY_CLASS}-item-data`; const ValidationSummary = CollectionWidget.inherit({ - _getDefaultOptions() { - return extend(this.callBase(), { - /** - * @name dxValidationSummaryOptions.focusStateEnabled - * @hidden - */ - focusStateEnabled: false, - /** - * @name dxValidationSummaryOptions.noDataText - * @hidden - */ - noDataText: null - - // Ignore comments - /** - * @name dxValidationSummaryOptions.dataSource - * @hidden - */ - - /** - * @name dxValidationSummaryOptions.activeStateEnabled - * @hidden - */ - /** - * @name dxValidationSummaryOptions.disabled - * @hidden - */ - /** - * @name dxValidationSummaryOptions.hint - * @hidden - */ - /** - * @name dxValidationSummaryOptions.itemHoldTimeout - * @hidden - */ - /** - * @name dxValidationSummaryOptions.rtlEnabled - * @hidden - */ - /** - * @name dxValidationSummaryOptions.selectedIndex - * @hidden - */ - /** - * @name dxValidationSummaryOptions.selectedItem - * @hidden - */ - /** - * @name dxValidationSummaryOptions.selectedItems - * @hidden - */ - - /** - * @name dxValidationSummaryOptions.selectedItemKeys - * @hidden - */ - - /** - * @name dxValidationSummaryOptions.keyExpr - * @hidden - */ - - /** - * @name dxValidationSummaryOptions.visible - * @hidden - */ - - /** - * @name dxValidationSummaryOptions.width - * @hidden - */ - /** - * @name dxValidationSummaryOptions.height - * @hidden - */ - - /** - * @name dxValidationSummaryOptions.onItemHold - * @hidden - * @action - */ - - /** - * @name dxValidationSummaryOptions.onItemRendered - * @hidden - * @action - */ - - /** - * @name dxValidationSummaryOptions.onItemSelect - * @hidden - * @action - */ - /** - * @name dxValidationSummaryOptions.onSelectionChanged - * @hidden - * @action - */ - - /** - * @name dxValidationSummaryOptions.onItemContextMenu - * @hidden - * @action - */ - - /** - * @name dxValidationSummaryOptions.accessKey - * @hidden - */ - - /** - * @name dxValidationSummaryOptions.tabIndex - * @hidden - */ - }); - }, - - _setOptionsByReference() { - this.callBase(); - - extend(this._optionsByReference, { - validationGroup: true - }); - }, - - _init() { - this.callBase(); - this._initGroupRegistration(); - }, - - _initGroupRegistration() { - const $element = this.$element(); - const group = this.option('validationGroup') || - ValidationEngine.findGroup($element, this._modelByElement($element)); - const groupConfig = ValidationEngine.addGroup(group); - - this._unsubscribeGroup(); - - this._groupWasInit = true; - this._validationGroup = group; - - this.groupSubscription = this._groupValidationHandler.bind(this); - groupConfig.on('validated', this.groupSubscription); - }, - - _unsubscribeGroup() { - const groupConfig = ValidationEngine.getGroupConfig(this._validationGroup); - groupConfig && groupConfig.off('validated', this.groupSubscription); - }, - - _getOrderedItems(validators, items) { - let orderedItems = []; - - each(validators, function(_, validator) { - const foundItems = grep(items, function(item) { - if(item.validator === validator) { - return true; - } - }); - - if(foundItems.length) { - orderedItems = orderedItems.concat(foundItems); - } - }); - - return orderedItems; - }, - - _groupValidationHandler(params) { - const items = this._getOrderedItems(params.validators, map(params.brokenRules, function(rule) { - return { - text: rule.message, - validator: rule.validator, - index: rule.index - }; - })); - - this.validators = params.validators; - - each(this.validators, (_, validator) => { - if(validator._validationSummary !== this) { - let handler = this._itemValidationHandler.bind(this); - const disposingHandler = function() { - validator.off('validated', handler); - validator._validationSummary = null; - handler = null; - }; - validator.on('validated', handler); - validator.on('disposing', disposingHandler); - validator._validationSummary = this; - } - }); - - this.option('items', items); - }, - - _itemValidationHandler({ isValid, validator, brokenRules }) { - let items = this.option('items'); - let itemsChanged = false; - - let itemIndex = 0; - while(itemIndex < items.length) { - const item = items[itemIndex]; - if(item.validator === validator) { - const foundRule = grep(brokenRules || [], function(rule) { - return rule.index === item.index; - })[0]; - if(isValid || !foundRule) { - items.splice(itemIndex, 1); - itemsChanged = true; - continue; - } - if(foundRule.message !== item.text) { - item.text = foundRule.message; - itemsChanged = true; - } - } - itemIndex++; + _getDefaultOptions() { + return extend(this.callBase(), { + focusStateEnabled: false, + noDataText: null, + }); + }, + + _setOptionsByReference() { + this.callBase(); + + extend(this._optionsByReference, { + validationGroup: true, + }); + }, + + _init() { + this.callBase(); + this._initGroupRegistration(); + }, + + _initGroupRegistration() { + const $element = this.$element(); + const group = this.option('validationGroup') + || ValidationEngine.findGroup($element, this._modelByElement($element)); + const groupConfig = ValidationEngine.addGroup(group); + + this._unsubscribeGroup(); + + this._groupWasInit = true; + this._validationGroup = group; + + this.groupSubscription = this._groupValidationHandler.bind(this); + groupConfig.on('validated', this.groupSubscription); + }, + + _unsubscribeGroup() { + const groupConfig = ValidationEngine.getGroupConfig(this._validationGroup); + groupConfig && groupConfig.off('validated', this.groupSubscription); + }, + + _getOrderedItems(validators, items) { + let orderedItems = []; + + each(validators, (_, validator) => { + // @ts-expect-error + const foundItems = grep(items, (item) => { + if (item.validator === validator) { + return true; } - each(brokenRules, function(_, rule) { - const foundItem = grep(items, function(item) { - return item.validator === validator && item.index === rule.index; - })[0]; - if(!foundItem) { - items.push({ - text: rule.message, - validator: validator, - index: rule.index - }); - itemsChanged = true; - } - }); - - if(itemsChanged) { - items = this._getOrderedItems(this.validators, items); - this.option('items', items); + }); + + if (foundItems.length) { + orderedItems = orderedItems.concat(foundItems); + } + }); + + return orderedItems; + }, + + _groupValidationHandler(params) { + const items = this._getOrderedItems(params.validators, map(params.brokenRules, (rule) => ({ + text: rule.message, + validator: rule.validator, + index: rule.index, + }))); + + this.validators = params.validators; + + each(this.validators, (_, validator) => { + if (validator._validationSummary !== this) { + let handler = this._itemValidationHandler.bind(this); + const disposingHandler = function () { + validator.off('validated', handler); + validator._validationSummary = null; + handler = null; + }; + validator.on('validated', handler); + validator.on('disposing', disposingHandler); + validator._validationSummary = this; + } + }); + + this.option('items', items); + }, + + _itemValidationHandler({ isValid, validator, brokenRules }) { + let items = this.option('items'); + let itemsChanged = false; + + let itemIndex = 0; + while (itemIndex < items.length) { + const item = items[itemIndex]; + if (item.validator === validator) { + const foundRule = grep(brokenRules || [], (rule) => rule.index === item.index)[0]; + if (isValid || !foundRule) { + items.splice(itemIndex, 1); + itemsChanged = true; + continue; } - }, - - _initMarkup() { - this.$element().addClass(VALIDATION_SUMMARY_CLASS); - this.callBase(); - }, - - _optionChanged(args) { - switch(args.name) { - case 'validationGroup': - this._initGroupRegistration(); - break; - default: - this.callBase(args); + if (foundRule.message !== item.text) { + item.text = foundRule.message; + itemsChanged = true; } - }, - - _itemClass() { - return ITEM_CLASS; - }, - - _itemDataKey() { - return ITEM_DATA_KEY; - }, - - _postprocessRenderItem(params) { - eventsEngine.on(params.itemElement, 'click', function() { - params.itemData.validator && params.itemData.validator.focus && params.itemData.validator.focus(); + } + itemIndex++; + } + each(brokenRules, (_, rule) => { + const foundItem = grep(items, (item) => item.validator === validator && item.index === rule.index)[0]; + if (!foundItem) { + items.push({ + text: rule.message, + validator, + index: rule.index, }); - }, + itemsChanged = true; + } + }); - _dispose() { - this.callBase(); - this._unsubscribeGroup(); - }, - - refreshValidationGroup() { - this._initGroupRegistration(); + if (itemsChanged) { + items = this._getOrderedItems(this.validators, items); + this.option('items', items); } + }, - /** - * @name dxValidationSummary.registerKeyHandler - * @publicName registerKeyHandler(key, handler) - * @hidden - */ - - /** - * @name dxValidationSummary.getDataSource - * @publicName getDataSource() - * @hidden - */ - - /** - * @name dxValidationSummary.focus - * @publicName focus() - * @hidden - */ + _initMarkup() { + this.$element().addClass(VALIDATION_SUMMARY_CLASS); + this.callBase(); + }, + _optionChanged(args) { + switch (args.name) { + case 'validationGroup': + this._initGroupRegistration(); + break; + default: + this.callBase(args); + } + }, + + _itemClass() { + return ITEM_CLASS; + }, + + _itemDataKey() { + return ITEM_DATA_KEY; + }, + + _postprocessRenderItem(params) { + eventsEngine.on(params.itemElement, 'click', () => { + params.itemData.validator && params.itemData.validator.focus && params.itemData.validator.focus(); + }); + }, + + _dispose() { + this.callBase(); + this._unsubscribeGroup(); + }, + + refreshValidationGroup() { + this._initGroupRegistration(); + }, }); registerComponent('dxValidationSummary', ValidationSummary); diff --git a/packages/devextreme/js/__internal/ui/m_validator.ts b/packages/devextreme/js/__internal/ui/m_validator.ts index d1a7b74808c9..c4accdbd5f91 100644 --- a/packages/devextreme/js/__internal/ui/m_validator.ts +++ b/packages/devextreme/js/__internal/ui/m_validator.ts @@ -1,293 +1,275 @@ -import { data as elementData } from '../core/element_data'; -import Callbacks from '../core/utils/callbacks'; -import errors from './widget/ui.errors'; -import DOMComponent from '../core/dom_component'; -import { extend } from '../core/utils/extend'; -import { map } from '../core/utils/iterator'; -import ValidationEngine from './validation_engine'; -import DefaultAdapter from './validation/default_adapter'; -import registerComponent from '../core/component_registrator'; -import { Deferred } from '../core/utils/deferred'; -import Guid from '../core/guid'; +import registerComponent from '@js/core/component_registrator'; +import DOMComponent from '@js/core/dom_component'; +import { data as elementData } from '@js/core/element_data'; +import Guid from '@js/core/guid'; +import Callbacks from '@js/core/utils/callbacks'; +import { Deferred } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { map } from '@js/core/utils/iterator'; +import errors from '@js/ui/widget/ui.errors'; + +import ValidationEngine from './m_validation_engine'; +import DefaultAdapter from './validation/m_default_adapter'; const VALIDATOR_CLASS = 'dx-validator'; const VALIDATION_STATUS_VALID = 'valid'; const VALIDATION_STATUS_INVALID = 'invalid'; const VALIDATION_STATUS_PENDING = 'pending'; - +// @ts-expect-error const Validator = DOMComponent.inherit({ - _initOptions: function(options) { - this.callBase.apply(this, arguments); - this.option(ValidationEngine.initValidationOptions(options)); - }, + _initOptions(options) { + this.callBase.apply(this, arguments); + this.option(ValidationEngine.initValidationOptions(options)); + }, - _getDefaultOptions() { - return extend(this.callBase(), { - validationRules: [] - /** - * @name dxValidatorOptions.rtlEnabled - * @hidden - */ - - /** - * @name dxValidator.beginUpdate - * @publicName beginUpdate() - * @hidden - */ - /** - * @name dxValidator.defaultOptions - * @publicName defaultOptions(rule) - * @hidden - */ - /** - * @name dxValidator.endUpdate - * @publicName endUpdate() - * @hidden - */ - }); - }, + _getDefaultOptions() { + return extend(this.callBase(), { + validationRules: [], + }); + }, - _init() { - this.callBase(); - this._initGroupRegistration(); - this.focused = Callbacks(); - this._initAdapter(); - this._validationInfo = { - result: null, - deferred: null, - skipValidation: false - }; - }, + _init() { + this.callBase(); + this._initGroupRegistration(); + this.focused = Callbacks(); + this._initAdapter(); + this._validationInfo = { + result: null, + deferred: null, + skipValidation: false, + }; + }, - _initGroupRegistration() { - const group = this._findGroup(); - if(!this._groupWasInit) { - this.on('disposing', function(args) { - ValidationEngine.removeRegisteredValidator(args.component._validationGroup, args.component); - }); - } - if(!this._groupWasInit || this._validationGroup !== group) { - ValidationEngine.removeRegisteredValidator(this._validationGroup, this); - this._groupWasInit = true; - this._validationGroup = group; - ValidationEngine.registerValidatorInGroup(group, this); - } - }, + _initGroupRegistration() { + const group = this._findGroup(); + if (!this._groupWasInit) { + this.on('disposing', (args) => { + ValidationEngine.removeRegisteredValidator(args.component._validationGroup, args.component); + }); + } + if (!this._groupWasInit || this._validationGroup !== group) { + ValidationEngine.removeRegisteredValidator(this._validationGroup, this); + this._groupWasInit = true; + this._validationGroup = group; + ValidationEngine.registerValidatorInGroup(group, this); + } + }, - _setOptionsByReference() { - this.callBase(); - extend(this._optionsByReference, { - validationGroup: true - }); - }, + _setOptionsByReference() { + this.callBase(); + extend(this._optionsByReference, { + validationGroup: true, + }); + }, - _getEditor() { - const element = this.$element()[0]; - return elementData(element, 'dx-validation-target'); - }, + _getEditor() { + const element = this.$element()[0]; + return elementData(element, 'dx-validation-target'); + }, - _initAdapter() { - const dxStandardEditor = this._getEditor(); - let adapter = this.option('adapter'); - if(!adapter) { - if(dxStandardEditor) { - adapter = new DefaultAdapter(dxStandardEditor, this); - adapter.validationRequestsCallbacks.push((args) => { - if(this._validationInfo.skipValidation) { - return; - } - this.validate(args); - }); - this.option('adapter', adapter); - return; - } - throw errors.Error('E0120'); - } - const callbacks = adapter.validationRequestsCallbacks; - if(callbacks) { - callbacks.push((args) => { - this.validate(args); - }); - } - }, + _initAdapter() { + const dxStandardEditor = this._getEditor(); + let adapter = this.option('adapter'); + if (!adapter) { + if (dxStandardEditor) { + adapter = new DefaultAdapter(dxStandardEditor, this); + adapter.validationRequestsCallbacks.push((args) => { + if (this._validationInfo.skipValidation) { + return; + } + this.validate(args); + }); + this.option('adapter', adapter); + return; + } + throw errors.Error('E0120'); + } + const callbacks = adapter.validationRequestsCallbacks; + if (callbacks) { + callbacks.push((args) => { + this.validate(args); + }); + } + }, - _toggleRTLDirection(isRtl) { - const rtlEnabled = this.option('adapter')?.editor?.option('rtlEnabled') ?? isRtl; + _toggleRTLDirection(isRtl) { + const rtlEnabled = this.option('adapter')?.editor?.option('rtlEnabled') ?? isRtl; - this.callBase(rtlEnabled); - }, + this.callBase(rtlEnabled); + }, - _initMarkup() { - this.$element().addClass(VALIDATOR_CLASS); - this.callBase(); - }, + _initMarkup() { + this.$element().addClass(VALIDATOR_CLASS); + this.callBase(); + }, - _render() { - this.callBase(); - this._toggleAccessibilityAttributes(); - }, + _render() { + this.callBase(); + this._toggleAccessibilityAttributes(); + }, - _toggleAccessibilityAttributes() { - const dxStandardEditor = this._getEditor(); - if(dxStandardEditor) { - const rules = this.option('validationRules') || []; - const isRequired = rules.some(({ type }) => type === 'required') || null; + _toggleAccessibilityAttributes() { + const dxStandardEditor = this._getEditor(); + if (dxStandardEditor) { + const rules = this.option('validationRules') || []; + const isRequired = rules.some(({ type }) => type === 'required') || null; - if(dxStandardEditor.isInitialized()) { - dxStandardEditor.setAria('required', isRequired); - } - dxStandardEditor.option('_onMarkupRendered', () => { - dxStandardEditor.setAria('required', isRequired); - }); - } - }, + if (dxStandardEditor.isInitialized()) { + dxStandardEditor.setAria('required', isRequired); + } + dxStandardEditor.option('_onMarkupRendered', () => { + dxStandardEditor.setAria('required', isRequired); + }); + } + }, - _visibilityChanged(visible) { - if(visible) { - this._initGroupRegistration(); - } - }, + _visibilityChanged(visible) { + if (visible) { + this._initGroupRegistration(); + } + }, - _optionChanged(args) { - switch(args.name) { - case 'validationGroup': - this._initGroupRegistration(); - return; - case 'validationRules': - this._resetValidationRules(); - this._toggleAccessibilityAttributes(); - this.option('isValid') !== undefined && this.validate(); - return; - case 'adapter': - this._initAdapter(); - break; - case 'isValid': - case 'validationStatus': - this.option(ValidationEngine.synchronizeValidationOptions(args, this.option())); - break; - default: - this.callBase(args); - } - }, + _optionChanged(args) { + switch (args.name) { + case 'validationGroup': + this._initGroupRegistration(); + return; + case 'validationRules': + this._resetValidationRules(); + this._toggleAccessibilityAttributes(); + this.option('isValid') !== undefined && this.validate(); + return; + case 'adapter': + this._initAdapter(); + break; + case 'isValid': + case 'validationStatus': + this.option(ValidationEngine.synchronizeValidationOptions(args, this.option())); + break; + default: + this.callBase(args); + } + }, - _getValidationRules() { - if(!this._validationRules) { - this._validationRules = map(this.option('validationRules'), (rule, index) => { - return extend({}, rule, { - validator: this, - index: index - }); - }); - } - return this._validationRules; - }, + _getValidationRules() { + if (!this._validationRules) { + this._validationRules = map(this.option('validationRules'), (rule, index) => extend({}, rule, { + validator: this, + index, + })); + } + return this._validationRules; + }, - _findGroup() { - const $element = this.$element(); + _findGroup() { + const $element = this.$element(); - return this.option('validationGroup') || - ValidationEngine.findGroup($element, this._modelByElement($element)); - }, + return this.option('validationGroup') + || ValidationEngine.findGroup($element, this._modelByElement($element)); + }, - _resetValidationRules() { - delete this._validationRules; - }, + _resetValidationRules() { + delete this._validationRules; + }, - validate(args) { - const adapter = this.option('adapter'); - const name = this.option('name'); - const bypass = adapter.bypass && adapter.bypass(); - const value = (args && args.value !== undefined) ? args.value : adapter.getValue(); - const currentError = adapter.getCurrentValidationError && adapter.getCurrentValidationError(); - const rules = this._getValidationRules(); - const currentResult = this._validationInfo && this._validationInfo.result; - if(currentResult && currentResult.status === VALIDATION_STATUS_PENDING && currentResult.value === value) { - return extend({}, currentResult); - } - let result; - if(bypass) { - result = { isValid: true, status: VALIDATION_STATUS_VALID }; - } else if(currentError && currentError.editorSpecific) { - currentError.validator = this; - result = { isValid: false, status: VALIDATION_STATUS_INVALID, brokenRule: currentError, brokenRules: [currentError] }; - } else { - result = ValidationEngine.validate(value, rules, name); - } - result.id = new Guid().toString(); - this._applyValidationResult(result, adapter); - result.complete && result.complete.then((res) => { - if(res.id === this._validationInfo.result.id) { - this._applyValidationResult(res, adapter); - } - }); - return extend({}, this._validationInfo.result); - }, + validate(args) { + const adapter = this.option('adapter'); + const name = this.option('name'); + const bypass = adapter.bypass && adapter.bypass(); + const value = args && args.value !== undefined ? args.value : adapter.getValue(); + const currentError = adapter.getCurrentValidationError && adapter.getCurrentValidationError(); + const rules = this._getValidationRules(); + const currentResult = this._validationInfo && this._validationInfo.result; + if (currentResult && currentResult.status === VALIDATION_STATUS_PENDING && currentResult.value === value) { + return extend({}, currentResult); + } + let result; + if (bypass) { + result = { isValid: true, status: VALIDATION_STATUS_VALID }; + } else if (currentError && currentError.editorSpecific) { + currentError.validator = this; + result = { + isValid: false, status: VALIDATION_STATUS_INVALID, brokenRule: currentError, brokenRules: [currentError], + }; + } else { + result = ValidationEngine.validate(value, rules, name); + } + result.id = new Guid().toString(); + this._applyValidationResult(result, adapter); + result.complete && result.complete.then((res) => { + if (res.id === this._validationInfo.result.id) { + this._applyValidationResult(res, adapter); + } + }); + return extend({}, this._validationInfo.result); + }, - reset() { - const adapter = this.option('adapter'); - const result = { - id: null, - isValid: true, - brokenRule: null, - brokenRules: null, - pendingRules: null, - status: VALIDATION_STATUS_VALID, - complete: null - }; + reset() { + const adapter = this.option('adapter'); + const result = { + id: null, + isValid: true, + brokenRule: null, + brokenRules: null, + pendingRules: null, + status: VALIDATION_STATUS_VALID, + complete: null, + }; - this._validationInfo.skipValidation = true; - adapter.reset(); - this._validationInfo.skipValidation = false; - this._resetValidationRules(); - this._applyValidationResult(result, adapter); - }, + this._validationInfo.skipValidation = true; + adapter.reset(); + this._validationInfo.skipValidation = false; + this._resetValidationRules(); + this._applyValidationResult(result, adapter); + }, - _updateValidationResult(result) { - if(!this._validationInfo.result || this._validationInfo.result.id !== result.id) { - const complete = this._validationInfo.deferred && this._validationInfo.result.complete; - this._validationInfo.result = extend({}, result, { complete }); - } else { - for(const prop in result) { - if(prop !== 'id' && prop !== 'complete') { - this._validationInfo.result[prop] = result[prop]; - } - } + _updateValidationResult(result) { + if (!this._validationInfo.result || this._validationInfo.result.id !== result.id) { + const complete = this._validationInfo.deferred && this._validationInfo.result.complete; + this._validationInfo.result = extend({}, result, { complete }); + } else { + // eslint-disable-next-line no-restricted-syntax + for (const prop in result) { + if (prop !== 'id' && prop !== 'complete') { + this._validationInfo.result[prop] = result[prop]; } - }, + } + } + }, - _applyValidationResult(result, adapter) { - const validatedAction = this._createActionByOption('onValidated', { - excludeValidators: ['readOnly'], - }); - result.validator = this; - this._updateValidationResult(result); - adapter.applyValidationResults && adapter.applyValidationResults(this._validationInfo.result); - this.option({ - validationStatus: this._validationInfo.result.status - }); - if(this._validationInfo.result.status === VALIDATION_STATUS_PENDING) { - if(!this._validationInfo.deferred) { - this._validationInfo.deferred = new Deferred(); - this._validationInfo.result.complete = this._validationInfo.deferred.promise(); - } - this._eventsStrategy.fireEvent('validating', [this._validationInfo.result]); - return; - } - if(this._validationInfo.result.status !== VALIDATION_STATUS_PENDING) { - validatedAction(result); - if(this._validationInfo.deferred) { - this._validationInfo.deferred.resolve(result); - this._validationInfo.deferred = null; - } - } - }, - focus() { - const adapter = this.option('adapter'); - adapter && adapter.focus && adapter.focus(); - }, - _useTemplates: function() { - return false; - }, + _applyValidationResult(result, adapter) { + const validatedAction = this._createActionByOption('onValidated', { + excludeValidators: ['readOnly'], + }); + result.validator = this; + this._updateValidationResult(result); + adapter.applyValidationResults && adapter.applyValidationResults(this._validationInfo.result); + this.option({ + validationStatus: this._validationInfo.result.status, + }); + if (this._validationInfo.result.status === VALIDATION_STATUS_PENDING) { + if (!this._validationInfo.deferred) { + this._validationInfo.deferred = Deferred(); + this._validationInfo.result.complete = this._validationInfo.deferred.promise(); + } + this._eventsStrategy.fireEvent('validating', [this._validationInfo.result]); + return; + } + if (this._validationInfo.result.status !== VALIDATION_STATUS_PENDING) { + validatedAction(result); + if (this._validationInfo.deferred) { + this._validationInfo.deferred.resolve(result); + this._validationInfo.deferred = null; + } + } + }, + focus() { + const adapter = this.option('adapter'); + adapter && adapter.focus && adapter.focus(); + }, + _useTemplates() { + return false; + }, }); registerComponent('dxValidator', Validator); diff --git a/packages/devextreme/js/__internal/ui/validation/m_default_adapter.ts b/packages/devextreme/js/__internal/ui/validation/m_default_adapter.ts index d70a4297e8e3..ceb4aee564f6 100644 --- a/packages/devextreme/js/__internal/ui/validation/m_default_adapter.ts +++ b/packages/devextreme/js/__internal/ui/validation/m_default_adapter.ts @@ -1,45 +1,45 @@ -import Class from '../../core/class'; +import Class from '@js/core/class'; const DefaultAdapter = Class.inherit({ - ctor(editor, validator) { - this.editor = editor; - this.validator = validator; - this.validationRequestsCallbacks = []; - const handler = (args) => { - this.validationRequestsCallbacks.forEach(item => item(args)); - }; - editor.validationRequest.add(handler); - editor.on('disposing', function() { - editor.validationRequest.remove(handler); - }); - }, - - getValue() { - return this.editor.option('value'); - }, - - getCurrentValidationError() { - return this.editor.option('validationError'); - }, - - bypass() { - return this.editor.option('disabled'); - }, - - applyValidationResults(params) { - this.editor.option({ - validationErrors: params.brokenRules, - validationStatus: params.status - }); - }, - - reset() { - this.editor.clear(); - }, - - focus() { - this.editor.focus(); - } + ctor(editor, validator) { + this.editor = editor; + this.validator = validator; + this.validationRequestsCallbacks = []; + const handler = (args) => { + this.validationRequestsCallbacks.forEach((item) => item(args)); + }; + editor.validationRequest.add(handler); + editor.on('disposing', () => { + editor.validationRequest.remove(handler); + }); + }, + + getValue() { + return this.editor.option('value'); + }, + + getCurrentValidationError() { + return this.editor.option('validationError'); + }, + + bypass() { + return this.editor.option('disabled'); + }, + + applyValidationResults(params) { + this.editor.option({ + validationErrors: params.brokenRules, + validationStatus: params.status, + }); + }, + + reset() { + this.editor.clear(); + }, + + focus() { + this.editor.focus(); + }, }); export default DefaultAdapter; diff --git a/packages/devextreme/js/ui/validation_engine.js b/packages/devextreme/js/ui/validation_engine.js new file mode 100644 index 000000000000..abe53beb550b --- /dev/null +++ b/packages/devextreme/js/ui/validation_engine.js @@ -0,0 +1,5 @@ +import ValidationEngine from '../__internal/ui/m_validation_engine'; + +export default ValidationEngine; + +// STYLE validation diff --git a/packages/devextreme/js/ui/validation_group.js b/packages/devextreme/js/ui/validation_group.js new file mode 100644 index 000000000000..f4872f348243 --- /dev/null +++ b/packages/devextreme/js/ui/validation_group.js @@ -0,0 +1,26 @@ +import ValidationGroup from '../__internal/ui/m_validation_group'; + +export default ValidationGroup; + +/** + * @name dxValidationGroupOptions.rtlEnabled + * @hidden + */ + +/** + * @name dxValidationGroup.beginUpdate + * @publicName beginUpdate() + * @hidden + */ + +/** + * @name dxValidationGroup.defaultOptions + * @publicName defaultOptions(rule) + * @hidden + */ + +/** + * @name dxValidationGroup.endUpdate + * @publicName endUpdate() + * @hidden + */ diff --git a/packages/devextreme/js/ui/validation_message.js b/packages/devextreme/js/ui/validation_message.js new file mode 100644 index 000000000000..dea2b44bbdbd --- /dev/null +++ b/packages/devextreme/js/ui/validation_message.js @@ -0,0 +1,3 @@ +import ValidationMessage from '../__internal/ui/m_validation_message'; + +export default ValidationMessage; diff --git a/packages/devextreme/js/ui/validation_summary.js b/packages/devextreme/js/ui/validation_summary.js new file mode 100644 index 000000000000..cc57924f14f6 --- /dev/null +++ b/packages/devextreme/js/ui/validation_summary.js @@ -0,0 +1,141 @@ +import ValidationSummary from '../__internal/ui/m_validation_summary'; + +export default ValidationSummary; + +/** + * @name dxValidationSummaryOptions.focusStateEnabled + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.noDataText + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.dataSource + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.activeStateEnabled + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.disabled + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.hint + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.itemHoldTimeout + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.rtlEnabled + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.selectedIndex + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.selectedItem + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.selectedItems + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.selectedItemKeys + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.keyExpr + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.visible + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.width + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.height + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.onItemHold + * @hidden + * @action + */ + +/** + * @name dxValidationSummaryOptions.onItemRendered + * @hidden + * @action + */ + +/** + * @name dxValidationSummaryOptions.onItemSelect + * @hidden + * @action + */ + +/** + * @name dxValidationSummaryOptions.onSelectionChanged + * @hidden + * @action + */ + +/** + * @name dxValidationSummaryOptions.onItemContextMenu + * @hidden + * @action + */ + +/** + * @name dxValidationSummaryOptions.accessKey + * @hidden + */ + +/** + * @name dxValidationSummaryOptions.tabIndex + * @hidden + */ + +/** + * @name dxValidationSummary.registerKeyHandler + * @publicName registerKeyHandler(key, handler) + * @hidden + */ + +/** + * @name dxValidationSummary.getDataSource + * @publicName getDataSource() + * @hidden + */ + +/** + * @name dxValidationSummary.focus + * @publicName focus() + * @hidden + */ diff --git a/packages/devextreme/js/ui/validator.js b/packages/devextreme/js/ui/validator.js new file mode 100644 index 000000000000..e4972cefe685 --- /dev/null +++ b/packages/devextreme/js/ui/validator.js @@ -0,0 +1,26 @@ +import Validator from '../__internal/ui/m_validator'; + +export default Validator; + +/** + * @name dxValidatorOptions.rtlEnabled + * @hidden + */ + +/** + * @name dxValidator.beginUpdate + * @publicName beginUpdate() + * @hidden + */ + +/** + * @name dxValidator.defaultOptions + * @publicName defaultOptions(rule) + * @hidden + */ + +/** + * @name dxValidator.endUpdate + * @publicName endUpdate() + * @hidden + */ diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.markup.tests.js index c0038ae3cc2d..55ac028813d9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.markup.tests.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Class from 'core/class'; -import DefaultAdapter from 'ui/validation/default_adapter'; +import DefaultAdapter from '__internal/ui/validation/m_default_adapter'; import ValidationEngine from 'ui/validation_engine'; import 'ui/validation_group'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.tests.js index 34ea57a5c157..a214d0b74d93 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationGroup.tests.js @@ -2,7 +2,7 @@ import Class from 'core/class'; import { Deferred } from 'core/utils/deferred'; import { triggerShownEvent } from 'events/visibility_change'; import $ from 'jquery'; -import DefaultAdapter from 'ui/validation/default_adapter'; +import DefaultAdapter from '__internal/ui/validation/m_default_adapter'; import ValidationEngine from 'ui/validation_engine'; import 'ui/validation_group'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js index a58b597152d5..c2fc8bb49abd 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Class from 'core/class'; -import DefaultAdapter from 'ui/validation/default_adapter'; +import DefaultAdapter from '__internal/ui/validation/m_default_adapter'; import ValidationEngine from 'ui/validation_engine'; import Validator from 'ui/validator'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.tests.js index 9d5a48c5bf17..34e6dbe7d973 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.tests.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Class from 'core/class'; -import DefaultAdapter from 'ui/validation/default_adapter'; +import DefaultAdapter from '__internal/ui/validation/m_default_adapter'; import ValidationEngine from 'ui/validation_engine'; import Validator from 'ui/validator'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.editors.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.editors.tests.js index c571ccc0a3cb..ad007cd00f40 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.editors.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.editors.tests.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import Class from 'core/class'; import Editor from 'ui/editor/editor'; -import DefaultAdapter from 'ui/validation/default_adapter'; +import DefaultAdapter from '__internal/ui/validation/m_default_adapter'; import ValidationEngine from 'ui/validation_engine'; import TextEditorBase from 'ui/text_box/ui.text_editor.base'; import { Deferred } from 'core/utils/deferred'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.tests.js index 0590ccc6fef3..714e3a78b193 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validator.tests.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import { noop } from 'core/utils/common'; import Class from 'core/class'; -import DefaultAdapter from 'ui/validation/default_adapter'; +import DefaultAdapter from '__internal/ui/validation/m_default_adapter'; import ValidationEngine from 'ui/validation_engine'; import { Deferred } from 'core/utils/deferred'; import { isPromise } from 'core/utils/type'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/button.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/button.tests.js index e63c863669f7..3537ddf8ba99 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/button.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/button.tests.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import renderer from 'core/renderer'; import ValidationEngine from 'ui/validation_engine'; import Validator from 'ui/validator'; -import DefaultAdapter from 'ui/validation/default_adapter'; +import DefaultAdapter from '__internal/ui/validation/m_default_adapter'; import keyboardMock from '../../helpers/keyboardMock.js'; import pointerMock from '../../helpers/pointerMock.js'; import * as checkStyleHelper from '../../helpers/checkStyleHelper.js'; From 9a35fd17c678c8f8b6ed2067d782444389efa0d0 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 13 Jun 2024 09:43:28 +0400 Subject: [PATCH 30/32] DeferRendering: move files to TS --- .../{ui/defer_rendering.js => __internal/ui/m_defer_rendering.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/devextreme/js/{ui/defer_rendering.js => __internal/ui/m_defer_rendering.ts} (100%) diff --git a/packages/devextreme/js/ui/defer_rendering.js b/packages/devextreme/js/__internal/ui/m_defer_rendering.ts similarity index 100% rename from packages/devextreme/js/ui/defer_rendering.js rename to packages/devextreme/js/__internal/ui/m_defer_rendering.ts From 304bc386a0a1a72b6b6e0017bb8cd7f97bd93e3e Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 13 Jun 2024 09:48:05 +0400 Subject: [PATCH 31/32] DeferRendering: ignore errors after move to TS --- .../js/__internal/ui/m_defer_rendering.ts | 492 +++++++++--------- packages/devextreme/js/ui/defer_rendering.js | 5 + 2 files changed, 255 insertions(+), 242 deletions(-) create mode 100644 packages/devextreme/js/ui/defer_rendering.js diff --git a/packages/devextreme/js/__internal/ui/m_defer_rendering.ts b/packages/devextreme/js/__internal/ui/m_defer_rendering.ts index 5e84ad58c22d..1894a410a47d 100644 --- a/packages/devextreme/js/__internal/ui/m_defer_rendering.ts +++ b/packages/devextreme/js/__internal/ui/m_defer_rendering.ts @@ -1,20 +1,26 @@ -import { TransitionExecutor } from '../animation/transition_executor/transition_executor'; -import registerComponent from '../core/component_registrator'; -import domAdapter from '../core/dom_adapter'; -import $ from '../core/renderer'; -import { noop, executeAsync } from '../core/utils/common'; -import { Deferred, fromPromise } from '../core/utils/deferred'; -import { extend } from '../core/utils/extend'; -import { each } from '../core/utils/iterator'; -import { isPromise } from '../core/utils/type'; -import { getWindow, hasWindow } from '../core/utils/window'; -import eventsEngine from '../events/core/events_engine'; -import { triggerShownEvent } from '../events/visibility_change'; -import LoadIndicator from './load_indicator'; -import Widget from './widget/ui.widget'; -import { getBoundingRect } from '../core/utils/position'; - -// STYLE deferRendering +import { TransitionExecutor } from '@js/animation/transition_executor/transition_executor'; +import registerComponent from '@js/core/component_registrator'; +import domAdapter from '@js/core/dom_adapter'; +import $ from '@js/core/renderer'; +import { + // @ts-expect-error + executeAsync, + noop, +} from '@js/core/utils/common'; +import { + Deferred, + // @ts-expect-error + fromPromise, +} from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { getBoundingRect } from '@js/core/utils/position'; +import { isPromise } from '@js/core/utils/type'; +import { getWindow, hasWindow } from '@js/core/utils/window'; +import eventsEngine from '@js/events/core/events_engine'; +import { triggerShownEvent } from '@js/events/visibility_change'; +import LoadIndicator from '@js/ui/load_indicator'; +import Widget from '@js/ui/widget/ui.widget'; const window = getWindow(); @@ -32,260 +38,262 @@ const DEFER_DEFER_RENDERING_LOAD_INDICATOR = 'dx-deferrendering-load-indicator'; const ANONYMOUS_TEMPLATE_NAME = 'content'; const ACTIONS = ['onRendered', 'onShown']; - +// @ts-expect-error const DeferRendering = Widget.inherit({ - _getDefaultOptions: function() { - return extend(this.callBase(), { - showLoadIndicator: false, - renderWhen: undefined, - animation: undefined, - staggerItemSelector: undefined, - onRendered: null, - onShown: null - }); - }, - - _getAnonymousTemplateName: function() { - return ANONYMOUS_TEMPLATE_NAME; - }, - - _init: function() { - this.transitionExecutor = new TransitionExecutor(); - - this._initElement(); - this._initRender(); - - this._$initialContent = this.$element().clone().contents(); - - this._initActions(); - this.callBase(); - }, - - _initElement: function() { - this.$element() - .addClass(DEFER_RENDERING_CLASS); - }, + _getDefaultOptions() { + return extend(this.callBase(), { + showLoadIndicator: false, + renderWhen: undefined, + animation: undefined, + staggerItemSelector: undefined, + onRendered: null, + onShown: null, + }); + }, + + _getAnonymousTemplateName() { + return ANONYMOUS_TEMPLATE_NAME; + }, + + _init() { + this.transitionExecutor = new TransitionExecutor(); + + this._initElement(); + this._initRender(); + + this._$initialContent = this.$element().clone().contents(); + + this._initActions(); + this.callBase(); + }, + + _initElement() { + this.$element() + .addClass(DEFER_RENDERING_CLASS); + }, + + _initRender() { + const that = this; + const $element = this.$element(); + const renderWhen = this.option('renderWhen'); + + const doRender = () => that._renderDeferredContent(); + + if (isPromise(renderWhen)) { + fromPromise(renderWhen).done(doRender); + } else { + $element.data('dx-render-delegate', doRender); + if (renderWhen === undefined) { + $element.addClass(PENDING_RENDERING_MANUAL_CLASS); + } + } + }, - _initRender: function() { - const that = this; - const $element = this.$element(); - const renderWhen = this.option('renderWhen'); + _initActions() { + this._actions = {}; - const doRender = () => { - return that._renderDeferredContent(); - }; + each(ACTIONS, (_, action) => { + this._actions[action] = this._createActionByOption(action) || noop; + }); + }, - if(isPromise(renderWhen)) { - fromPromise(renderWhen).done(doRender); - } else { - $element.data('dx-render-delegate', doRender); - if(renderWhen === undefined) { - $element.addClass(PENDING_RENDERING_MANUAL_CLASS); - } - } - }, + _initMarkup() { + this.callBase(); - _initActions: function() { - this._actions = {}; + if (!this._initContent) { + this._initContent = this._renderContent; + this._renderContent = () => {}; + } - each(ACTIONS, (_, action) => { - this._actions[action] = this._createActionByOption(action) || noop; + this._initContent(); + }, + + _renderContentImpl() { + this.$element().removeClass(WIDGET_CLASS); + this.$element().append(this._$initialContent); + this._setLoadingState(); + }, + + _renderDeferredContent() { + const that = this; + const $element = this.$element(); + const result = Deferred(); + + $element.removeClass(PENDING_RENDERING_MANUAL_CLASS); + $element.addClass(PENDING_RENDERING_ACTIVE_CLASS); + + this._abortRenderTask(); + this._renderTask = executeAsync(() => { + that._renderImpl() + .done(() => { + const shownArgs = { element: $element }; + that._actions.onShown([shownArgs]); + result.resolve(shownArgs); + }) + .fail(function () { + // @ts-expect-error + result.rejectWith(result, arguments); }); - }, - - - _initMarkup: function() { - this.callBase(); - - if(!this._initContent) { - this._initContent = this._renderContent; - this._renderContent = () => {}; - } - - this._initContent(); - }, - - _renderContentImpl: function() { - this.$element().removeClass(WIDGET_CLASS); - this.$element().append(this._$initialContent); - this._setLoadingState(); - }, - - _renderDeferredContent: function() { - const that = this; - const $element = this.$element(); - const result = new Deferred(); - - $element.removeClass(PENDING_RENDERING_MANUAL_CLASS); - $element.addClass(PENDING_RENDERING_ACTIVE_CLASS); - - this._abortRenderTask(); - this._renderTask = executeAsync(() => { - that._renderImpl() - .done(() => { - const shownArgs = { element: $element }; - that._actions.onShown([shownArgs]); - result.resolve(shownArgs); - }) - .fail(function() { - result.rejectWith(result, arguments); - }); + }); + + return result.promise(); + }, + + _isElementInViewport(element) { + const rect = getBoundingRect(element); + + return rect.bottom >= 0 + && rect.right >= 0 + && rect.top <= (window.innerHeight || domAdapter.getDocumentElement().clientHeight) + && rect.left <= (window.innerWidth || domAdapter.getDocumentElement().clientWidth); + }, + + _animate() { + const that = this; + const $element = this.$element(); + const animation = hasWindow() && this.option('animation'); + const staggerItemSelector = this.option('staggerItemSelector'); + let animatePromise; + + that.transitionExecutor.stop(); + + if (animation) { + if (staggerItemSelector) { + $element.find(staggerItemSelector).each(function () { + if (that._isElementInViewport(this)) { + that.transitionExecutor.enter($(this), animation); + } }); + } else { + that.transitionExecutor.enter($element, animation); + } + animatePromise = that.transitionExecutor.start(); + } else { + animatePromise = Deferred().resolve().promise(); + } - return result.promise(); - }, - - _isElementInViewport: function(element) { - const rect = getBoundingRect(element); - - return rect.bottom >= 0 && - rect.right >= 0 && - rect.top <= (window.innerHeight || domAdapter.getDocumentElement().clientHeight) && - rect.left <= (window.innerWidth || domAdapter.getDocumentElement().clientWidth); - }, - - _animate: function() { - const that = this; - const $element = this.$element(); - const animation = hasWindow() && this.option('animation'); - const staggerItemSelector = this.option('staggerItemSelector'); - let animatePromise; - - that.transitionExecutor.stop(); - - if(animation) { - if(staggerItemSelector) { - $element.find(staggerItemSelector).each(function() { - if(that._isElementInViewport(this)) { - that.transitionExecutor.enter($(this), animation); - } - }); - } else { - that.transitionExecutor.enter($element, animation); - } - animatePromise = that.transitionExecutor.start(); - } else { - animatePromise = new Deferred().resolve().promise(); - } - - return animatePromise; - }, - - _renderImpl: function() { - const $element = this.$element(); - const renderedArgs = { - element: $element - }; + return animatePromise; + }, - const contentTemplate = this._getTemplate(this._templateManager.anonymousTemplateName); + _renderImpl() { + const $element = this.$element(); + const renderedArgs = { + element: $element, + }; - if(contentTemplate) { - contentTemplate.render({ - container: $element.empty(), - noModel: true - }); - } + const contentTemplate = this._getTemplate(this._templateManager.anonymousTemplateName); - this._setRenderedState($element); - eventsEngine.trigger($element, 'dxcontentrendered'); - this._actions.onRendered([renderedArgs]); - this._isRendered = true; + if (contentTemplate) { + contentTemplate.render({ + container: $element.empty(), + noModel: true, + }); + } - return this._animate(); - }, + this._setRenderedState($element); + // @ts-expect-error + eventsEngine.trigger($element, 'dxcontentrendered'); + this._actions.onRendered([renderedArgs]); + this._isRendered = true; - _setLoadingState: function() { - const $element = this.$element(); - const hasCustomLoadIndicator = !!$element.find('.' + VISIBLE_WHILE_PENDING_RENDERING_CLASS).length; + return this._animate(); + }, - $element.addClass(PENDING_RENDERING_CLASS); - if(!hasCustomLoadIndicator) { - $element.children().addClass(INVISIBLE_WHILE_PENDING_RENDERING_CLASS); - } + _setLoadingState() { + const $element = this.$element(); + const hasCustomLoadIndicator = !!$element.find(`.${VISIBLE_WHILE_PENDING_RENDERING_CLASS}`).length; - if(this.option('showLoadIndicator')) { - this._showLoadIndicator($element); - } - }, + $element.addClass(PENDING_RENDERING_CLASS); + if (!hasCustomLoadIndicator) { + $element.children().addClass(INVISIBLE_WHILE_PENDING_RENDERING_CLASS); + } - _showLoadIndicator: function($container) { - this._$loadIndicator = new LoadIndicator($('
'), { visible: true }).$element() - .addClass(DEFER_DEFER_RENDERING_LOAD_INDICATOR); + if (this.option('showLoadIndicator')) { + this._showLoadIndicator($element); + } + }, + + _showLoadIndicator($container) { + // @ts-expect-error + this._$loadIndicator = new LoadIndicator($('
'), { visible: true }) + .$element() + // @ts-expect-error + .addClass(DEFER_DEFER_RENDERING_LOAD_INDICATOR); + + $('
') + .addClass(LOADINDICATOR_CONTAINER_CLASS) + .addClass(DEFER_RENDERING_LOADINDICATOR_CONTAINER_CLASS) + .append(this._$loadIndicator) + .appendTo($container); + }, + + _setRenderedState() { + const $element = this.$element(); + + if (this._$loadIndicator) { + this._$loadIndicator.remove(); + } - $('
') - .addClass(LOADINDICATOR_CONTAINER_CLASS) - .addClass(DEFER_RENDERING_LOADINDICATOR_CONTAINER_CLASS) - .append(this._$loadIndicator) - .appendTo($container); - }, + $element.removeClass(PENDING_RENDERING_CLASS); + $element.removeClass(PENDING_RENDERING_ACTIVE_CLASS); - _setRenderedState: function() { - const $element = this.$element(); + triggerShownEvent($element.children()); + }, - if(this._$loadIndicator) { - this._$loadIndicator.remove(); - } + _optionChanged(args) { + const { value } = args; + const { previousValue } = args; - $element.removeClass(PENDING_RENDERING_CLASS); - $element.removeClass(PENDING_RENDERING_ACTIVE_CLASS); - - triggerShownEvent($element.children()); - }, - - _optionChanged: function(args) { - const value = args.value; - const previousValue = args.previousValue; - - switch(args.name) { - case 'renderWhen': - if(previousValue === false && value === true) { - this._renderOrAnimate(); - } else if(previousValue === true && value === false) { - this.transitionExecutor.stop(); - this._setLoadingState(); - } - break; - case 'showLoadIndicator': - case 'onRendered': - case 'onShown': - break; - default: - this.callBase(args); + switch (args.name) { + case 'renderWhen': + if (previousValue === false && value === true) { + this._renderOrAnimate(); + } else if (previousValue === true && value === false) { + this.transitionExecutor.stop(); + this._setLoadingState(); } - }, + break; + case 'showLoadIndicator': + case 'onRendered': + case 'onShown': + break; + default: + this.callBase(args); + } + }, - _renderOrAnimate: function() { - let result; + _renderOrAnimate() { + let result; - if(this._isRendered) { - this._setRenderedState(); - result = this._animate(); - } else { - result = this._renderDeferredContent(); - } + if (this._isRendered) { + this._setRenderedState(); + result = this._animate(); + } else { + result = this._renderDeferredContent(); + } - return result; - }, + return result; + }, - renderContent: function() { - return this._renderOrAnimate(); - }, + renderContent() { + return this._renderOrAnimate(); + }, - _abortRenderTask: function() { - if(this._renderTask) { - this._renderTask.abort(); - this._renderTask = undefined; - } - }, - - _dispose: function() { - this.transitionExecutor.stop(true); - this._abortRenderTask(); - this._actions = undefined; - this._$initialContent = undefined; - this.callBase(); + _abortRenderTask() { + if (this._renderTask) { + this._renderTask.abort(); + this._renderTask = undefined; } + }, + + _dispose() { + this.transitionExecutor.stop(true); + this._abortRenderTask(); + this._actions = undefined; + this._$initialContent = undefined; + this.callBase(); + }, }); diff --git a/packages/devextreme/js/ui/defer_rendering.js b/packages/devextreme/js/ui/defer_rendering.js new file mode 100644 index 000000000000..1722cff85c82 --- /dev/null +++ b/packages/devextreme/js/ui/defer_rendering.js @@ -0,0 +1,5 @@ +import DeferRendering from '../__internal/ui/m_defer_rendering'; + +export default DeferRendering; + +// STYLE deferRendering From 4509333a21ad60c4d4353926d4a77daee5ce0754 Mon Sep 17 00:00:00 2001 From: Nikki Gonzales <38495263+nikkithelegendarypokemonster@users.noreply.github.com> Date: Thu, 13 Jun 2024 15:08:35 +0800 Subject: [PATCH 32/32] Tabs/Selection: Corrected classname appending in Vue template by adding wrapper div (#27555) --- apps/demos/Demos/Tabs/Selection/Vue/App.vue | 22 ++++++++++--------- apps/demos/testing/common.test.js | 3 --- .../utils/visual-tests/matrix-test-helper.js | 3 --- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/apps/demos/Demos/Tabs/Selection/Vue/App.vue b/apps/demos/Demos/Tabs/Selection/Vue/App.vue index e49e4aea7883..4b51597d92bc 100644 --- a/apps/demos/Demos/Tabs/Selection/Vue/App.vue +++ b/apps/demos/Demos/Tabs/Selection/Vue/App.vue @@ -31,16 +31,18 @@ :animation-enabled="true" > diff --git a/apps/demos/testing/common.test.js b/apps/demos/testing/common.test.js index 230a28d04432..00ed0e5eb6e0 100644 --- a/apps/demos/testing/common.test.js +++ b/apps/demos/testing/common.test.js @@ -206,9 +206,6 @@ const SKIPPED_TESTS = { TreeList: [ { demo: 'Overview', themes: [THEME.material] }, ], - Tabs: [ - { demo: 'Selection', themes: [THEME.material, THEME.fluent] }, - ], List: [ { demo: 'ListSelection', themes: [THEME.material] }, { demo: 'ListWithSearchBar', themes: [THEME.material] }, diff --git a/apps/demos/utils/visual-tests/matrix-test-helper.js b/apps/demos/utils/visual-tests/matrix-test-helper.js index aeedc1988d05..ecdf30d227a0 100644 --- a/apps/demos/utils/visual-tests/matrix-test-helper.js +++ b/apps/demos/utils/visual-tests/matrix-test-helper.js @@ -245,9 +245,6 @@ const SKIPPED_TESTS = { List: [ { demo: 'ListSelection', themes: [THEME.material] }, ], - Tabs: [ - { demo: 'Selection', themes: [THEME.fluent, THEME.material] }, - ], TabPanel: [ { demo: 'Overview', themes: [THEME.material] }, ],