├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github ├── renovate.json5 └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── addon └── index.ts ├── app └── .gitkeep ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── dummy │ ├── app │ │ ├── app.ts │ │ ├── components │ │ │ └── .gitkeep │ │ ├── config │ │ │ └── environment.d.ts │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── router.ts │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ └── application.hbs │ ├── config │ │ ├── ember-cli-update.json │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ └── robots.txt ├── index.html ├── integration │ ├── -helpers.ts │ └── hash-test.ts └── test-helper.ts ├── tsconfig.json ├── types ├── dummy │ └── index.d.ts └── global.d.ts ├── vendor └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { configs } = require('@nullvoxpopuli/eslint-configs'); 4 | 5 | module.exports = configs.ember(); 6 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | // Docs: 2 | // https://docs.renovatebot.com/configuration-options/ 3 | { 4 | "extends": [ 5 | "config:base", 6 | ":semanticCommits" 7 | ], 8 | "automerge": true, 9 | "masterIssue": true, 10 | "rangeStrategy": "bump", 11 | "packageRules": [ 12 | //////////////////////////////////////// 13 | // Grouping namespaced packages together 14 | // 15 | // This reduces overall PR count 16 | //////////////////////////////////////// 17 | { 18 | "groupName": "Type Definitions", 19 | "packagePatterns": ["^@types\/*"], 20 | "schedule": ["after 9pm on sunday"], 21 | }, 22 | { 23 | "groupName": "Lint Dependencies", 24 | "schedule": ["after 9pm on sunday"], 25 | "packageNames": [ 26 | "eslint", 27 | "babel-eslint", 28 | "ember-template-lint", 29 | "prettier" 30 | ], 31 | "packagePatterns": [ 32 | "eslint-plugin-.*", 33 | "eslint-config-.*", 34 | ".*typescript-eslint.*", 35 | "^@commitlint\/*", 36 | "^remark-*" 37 | ] 38 | }, 39 | // These are dependencies that come default when 40 | // generating a new ember addon 41 | { 42 | "groupName": "Framework Dependencies", 43 | "packageNames": [ 44 | "@ember/optional-features", 45 | "@glimmer/component", 46 | "@glimmer/tracking", 47 | "ember-disable-prototype-extensions", 48 | "ember-export-application-global", 49 | "ember-load-initializers", 50 | "ember-maybe-import-regenerator", 51 | "ember-resolver", 52 | "ember-source", 53 | "ember-cli-page-title" 54 | ] 55 | }, 56 | { 57 | "groupName": "CLI Dependencies", 58 | "packageNames": [ 59 | "broccoli-asset-rev", 60 | "ember-cli", 61 | "ember-auto-import", 62 | "ember-cli-dependency-checker", 63 | "ember-cli-inject-live-reload", 64 | "ember-cli-sri", 65 | "ember-cli-terser" 66 | ] 67 | }, 68 | { 69 | "groupName": "Testing Dependencies", 70 | "schedule": ["after 9pm on sunday"], 71 | "packageNames": [ 72 | "qunit-dom", 73 | "ember-try", 74 | "ember-source-channel-url", 75 | "ember-qunit", 76 | "qunit", 77 | "npm-run-all" 78 | ] 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | schedule: 10 | - cron: "0 3 * * 0" # every Sunday at 3am 11 | 12 | env: 13 | CI: true 14 | 15 | jobs: 16 | type_checking: 17 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 18 | name: Check Types 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: volta-cli/action@v1 24 | 25 | - run: yarn install --frozen-lockfile 26 | - run: yarn prepack 27 | 28 | 29 | tests: 30 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 31 | name: Base Tests 32 | timeout-minutes: 5 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | node: 37 | - "16" 38 | - "18" 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: volta-cli/action@v1 42 | with: 43 | node-version: ${{ matrix.node }} 44 | 45 | - run: yarn install --frozen-lockfile 46 | 47 | - name: Test with ${{ matrix.node }} 48 | run: yarn ember test 49 | 50 | floating-dependencies: 51 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 52 | name: Floating Dependencies 53 | timeout-minutes: 5 54 | runs-on: ubuntu-latest 55 | strategy: 56 | matrix: 57 | node: 58 | - "16" 59 | - "18" 60 | 61 | steps: 62 | - uses: actions/checkout@v3 63 | - uses: volta-cli/action@v1 64 | with: 65 | node-version: ${{ matrix.node }} 66 | 67 | - run: yarn install --no-lockfile 68 | 69 | - name: Test with Node ${{ matrix.node }} 70 | run: yarn ember test 71 | 72 | try-scenarios: 73 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 74 | name: "Compatibility" 75 | timeout-minutes: 7 76 | runs-on: ubuntu-latest 77 | needs: [ tests, type_checking ] 78 | 79 | strategy: 80 | fail-fast: true 81 | matrix: 82 | ember-try-scenario: 83 | - ember-4.4 84 | - ember-4.8 85 | - ember-release 86 | - ember-beta 87 | - ember-canary 88 | - embroider 89 | steps: 90 | - uses: actions/checkout@v3 91 | - uses: volta-cli/action@v1 92 | with: 93 | node-version: 16.x 94 | - name: install dependencies 95 | run: yarn install --frozen-lockfile 96 | - name: test 97 | run: node_modules/.bin/ember try:one ${{ matrix.ember-try-scenario }} --skip-cleanup 98 | 99 | ember-cli-update: 100 | if: github.event_name == 'pull_request' && github.event.pusher.name == 'renovate-bot' 101 | runs-on: ubuntu-latest 102 | needs: [tests, try-scenarios, floating-dependencies] 103 | 104 | steps: 105 | - uses: actions/checkout@v3 106 | with: 107 | ref: ${{ github.head_ref }} 108 | token: ${{ secrets.GitHubToken }} 109 | - uses: actions/setup-node@v3 110 | - uses: kellyselden/ember-cli-update-action@v3 111 | with: 112 | autofix_command: yarn lint:fix 113 | ignore_to: true 114 | 115 | publish: 116 | name: Release 117 | runs-on: ubuntu-latest 118 | if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' 119 | needs: [tests, try-scenarios, floating-dependencies] 120 | 121 | steps: 122 | - uses: actions/checkout@v3 123 | with: 124 | persist-credentials: false 125 | - uses: volta-cli/action@v1 126 | - run: yarn install 127 | 128 | - name: Release 129 | run: yarn semantic-release 130 | env: 131 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 132 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 133 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | # based on: 4 | # - https://github.com/NullVoxPopuli/eslint-plugin-decorator-position/blob/master/.github/workflows/lint.yml 5 | # - https://github.com/NullVoxPopuli/ember-autostash-modifier/blob/master/.github/workflows/ci.yml 6 | # - https://github.com/emberjs/ember-test-helpers/blob/master/.github/workflows/ci-build.yml 7 | on: 8 | pull_request: 9 | push: 10 | # filtering branches here prevents duplicate builds from pull_request and push 11 | branches: 12 | - main 13 | 14 | env: 15 | CI: true 16 | 17 | jobs: 18 | source: 19 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 20 | name: Source 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: volta-cli/action@v1 26 | 27 | - run: yarn install --frozen-lockfile 28 | 29 | - name: ESLint 30 | run: yarn lint:js 31 | 32 | - name: Templates 33 | run: yarn lint:hbs 34 | 35 | 36 | # docs: 37 | # if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 38 | # name: Docs 39 | # runs-on: ubuntu-latest 40 | 41 | # steps: 42 | # - uses: actions/checkout@v2 43 | # - uses: volta-cli/action@v1 44 | 45 | # - run: yarn install 46 | # - run: yarn lint:docs 47 | 48 | 49 | # docs-js-code: 50 | # if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 51 | # name: Docs (JS Code Samples) 52 | # runs-on: ubuntu-latest 53 | 54 | # steps: 55 | # - uses: actions/checkout@v2 56 | # - uses: volta-cli/action@v1 57 | 58 | # - run: yarn install 59 | # - run: yarn lint:docs-js 60 | 61 | commits: 62 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 63 | name: Commit Messages 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - uses: actions/checkout@v3 68 | with: 69 | fetch-depth: 0 70 | 71 | - uses: volta-cli/action@v1 72 | - uses: wagoid/commitlint-github-action@v4.1.15 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.template-lintrc.js 19 | /.travis.yml 20 | /.watchmanconfig 21 | /bower.json 22 | /config/ember-try.js 23 | /CONTRIBUTING.md 24 | /ember-cli-build.js 25 | /testem.js 26 | /tests/ 27 | /yarn.lock 28 | .gitkeep 29 | 30 | # ember-try 31 | /.node_modules.ember-try/ 32 | /bower.json.ember-try 33 | /package.json.ember-try 34 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane', 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "14" 7 | 8 | dist: xenial 9 | 10 | addons: 11 | chrome: stable 12 | 13 | cache: 14 | directories: 15 | - $HOME/.npm 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | 22 | branches: 23 | only: 24 | - master 25 | # npm version tags 26 | - /^v\d+\.\d+\.\d+/ 27 | 28 | jobs: 29 | fast_finish: true 30 | allow_failures: 31 | - env: EMBER_TRY_SCENARIO=ember-canary 32 | 33 | include: 34 | # runs linting and tests with current locked deps 35 | - stage: "Tests" 36 | name: "Tests" 37 | script: 38 | - npm run lint 39 | - npm run test:ember 40 | 41 | - stage: "Additional Tests" 42 | name: "Floating Dependencies" 43 | install: 44 | - npm install --no-package-lock 45 | script: 46 | - npm run test:ember 47 | 48 | # we recommend new addons test the current and previous LTS 49 | # as well as latest stable release (bonus points to beta/canary) 50 | - env: EMBER_TRY_SCENARIO=ember-lts-3.16 51 | - env: EMBER_TRY_SCENARIO=ember-lts-3.20 52 | - env: EMBER_TRY_SCENARIO=ember-release 53 | - env: EMBER_TRY_SCENARIO=ember-beta 54 | - env: EMBER_TRY_SCENARIO=ember-canary 55 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 56 | - env: EMBER_TRY_SCENARIO=ember-classic 57 | 58 | script: 59 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 60 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.11](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.10...v1.0.11) (2023-05-11) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependency ember-cli-typescript to v5 ([18051b9](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/18051b92140ac661dfcde87cd214a77f77c94fee)) 7 | 8 | ## [1.0.10](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.9...v1.0.10) (2023-05-11) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency ember-cli-htmlbars to ^6.2.0 ([e59aefe](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/e59aefe66db24d1e9116c978de9b02d2e865a29b)) 14 | 15 | ## [1.0.9](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.8...v1.0.9) (2023-05-10) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * add missing @types/ember__destroyable dependency ([9f61791](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/9f61791b4022e72b4b89c05c641dfabc64b123a4)) 21 | * add webpack@5 to fix "ember test" ([29c2e51](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/29c2e51673e98565a2e6ac7212278a1231a01820)) 22 | * bump @types/ember-qunit to fix typechecking ([7b15be2](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/7b15be23117722cdaa7b0104d6ddfad3a1ff7a3c)) 23 | * dedup "@types/ember" packages ([9d64849](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/9d64849bd07b74c74dc948e356965fa93edcf20b)) 24 | * dedup @types/eslint to prevent type mismatch ([3191338](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/319133833458b86c821f6bddb17f85f14a71b2e2)) 25 | * **deps:** update dependency ember-cli-babel to ^7.26.11 ([6a9f076](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/6a9f076b6a4c3272e0f105b1a4239f7d8439a157)) 26 | * **deps:** update dependency ember-cli-htmlbars to ^5.7.2 ([55b44e5](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/55b44e5d42fdad2da05b4765eb4fccef9106df52)) 27 | * **deps:** update dependency ember-cli-htmlbars to v6 ([1256130](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/1256130adce4f59fad9c69b0c9dc97408e0ea814)) 28 | * ignore loading route when handling initial page load ([853ea7a](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/853ea7a1fc99273c21526f01cfc3d1ec4378ad6d)) 29 | * update ember-try to fix local run ([5d4c880](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/5d4c880b18b8e890622ac3d7e9044d382e17c71e)) 30 | * update node version for CI ([80554ec](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/80554ec72b3de2e8cb98c942cc2fbe68018c32de)) 31 | 32 | ## [1.0.8](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.7...v1.0.8) (2021-06-17) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **deps:** update dependency ember-cli-typescript to ^4.2.1 ([42b2b11](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/42b2b115e2e94338c780e32a4aec1f345adbca9f)) 38 | 39 | ## [1.0.7](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.6...v1.0.7) (2021-06-15) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * **deps:** update dependency ember-cli-typescript to ^4.2.0 ([819a2c1](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/819a2c15cbd759f7835d46f90a1b7a97d35a0171)) 45 | 46 | ## [1.0.6](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.5...v1.0.6) (2021-05-26) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **types:** withHashSupport needs explicit type ([5d447ef](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/5d447ef927cad29d97940355c2afd160d359a02a)) 52 | 53 | ## [1.0.5](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.4...v1.0.5) (2021-05-26) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * intent is not always present, like when during qp-only transitions ([fbc7c3c](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/fbc7c3c66e9bcbc6eed382e7514bbe943af2ed7c)) 59 | 60 | ## [1.0.4](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.3...v1.0.4) (2021-05-18) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * **deps:** update dependency ember-cli-babel to ^7.26.6 ([4d0c977](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/4d0c9771733307726c7be7ae5c59967c2a085b92)) 66 | 67 | ## [1.0.3](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.2...v1.0.3) (2021-04-28) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * **deps:** update dependency ember-cli-babel to ^7.26.4 ([8cd88ec](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/8cd88ecfdcb59261fb476e3bece9aa4f34e8d741)) 73 | 74 | ## [1.0.2](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.1...v1.0.2) (2021-04-24) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * **deps:** update dependency ember-cli-babel to ^7.26.3 ([fd50c80](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/fd50c80eeb7ddcbf5eeefbf344355be8724e5f02)) 80 | 81 | ## [1.0.1](https://github.com/CrowdStrike/ember-url-hash-polyfill/compare/v1.0.0...v1.0.1) (2021-04-24) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * **deps:** update dependency ember-cli-htmlbars to ^5.7.1 ([84d36b7](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/84d36b7e23d9ae76ef803bfe0a737351f26cc064)) 87 | 88 | # 1.0.0 (2021-04-23) 89 | 90 | 91 | ### Features 92 | 93 | * initial implementation ([86cad0c](https://github.com/CrowdStrike/ember-url-hash-polyfill/commit/86cad0cbcb0d6833fdb4016ef89a75bc488792d0)) 94 | 95 | 96 | ### BREAKING CHANGES 97 | 98 | * initial release prep 99 | (actually implement the behavior) 100 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-url-hash-polyfill` 7 | * `npm install` 8 | 9 | ## Linting 10 | 11 | * `npm run lint:hbs` 12 | * `npm run lint:js` 13 | * `npm run lint:js -- --fix` 14 | 15 | ## Running tests 16 | 17 | * `ember test` – Runs the test suite on the current Ember version 18 | * `ember test --server` – Runs the test suite in "watch mode" 19 | * `ember try:each` – Runs the test suite against multiple Ember versions 20 | 21 | ## Running the dummy application 22 | 23 | * `ember serve` 24 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 25 | 26 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ember-url-hash-polyfill 2 | ============================================================================== 3 | 4 | Navigating to URLs with `#hash-targets` in them is not supported by 5 | most single-page-app frameworks due to the async rendering nature of 6 | modern web apps -- the browser can't scroll to a `#hash-target` on 7 | page load / transition because the element hasn't rendered yet. 8 | There is an issue about this for Ember 9 | [here on the RFCs repo](https://github.com/emberjs/rfcs/issues/709). 10 | 11 | This addon provides a way to support the behavior that is in normally 12 | native to browsers where an anchor tag with `href="#some-id-or-name"` 13 | would scroll down the page when clicked. 14 | 15 | ## Installation 16 | 17 | ``` 18 | yarn add ember-url-hash-polyfill 19 | # or 20 | npm install ember-url-hash-polyfill 21 | # or 22 | ember install ember-url-hash-polyfill 23 | ``` 24 | 25 | ## Compatibility 26 | 27 | * Ember.js v3.25 or above 28 | * Node.js v14 or above 29 | 30 | ## Usage 31 | 32 | To handle `/some-url/#hash-targets` on page load and after normal route transitions, 33 | ```diff 34 | // app/router.js 35 | 36 | import { withHashSupport } from 'ember-url-hash-polyfill'; 37 | 38 | + @withHashSupport 39 | export default class Router extends EmberRouter { 40 | location = config.locationType; 41 | rootURL = config.rootURL; 42 | } 43 | ``` 44 | 45 | Additionally, there is a `scrollToHash` helper if manual invocation is desired. 46 | 47 | ```js 48 | import { scrollToHash } from 'ember-url-hash-polyfill'; 49 | 50 | // ... 51 | 52 | scrollToHash('some-element-id-or-name'); 53 | ``` 54 | 55 | 56 | ## Contributing 57 | 58 | See the [Contributing](CONTRIBUTING.md) guide for details. 59 | 60 | 61 | ## License 62 | 63 | This project is licensed under the [MIT License](LICENSE.md). 64 | -------------------------------------------------------------------------------- /addon/index.ts: -------------------------------------------------------------------------------- 1 | import { getOwner } from '@ember/application'; 2 | import { warn } from '@ember/debug'; 3 | import { isDestroyed, isDestroying, registerDestructor } from '@ember/destroyable'; 4 | import { schedule } from '@ember/runloop'; 5 | import { waitForPromise } from '@ember/test-waiters'; 6 | 7 | import type ApplicationInstance from '@ember/application/instance'; 8 | import type { Route } from '@ember/routing'; 9 | import type EmberRouter from '@ember/routing/router'; 10 | import type RouterService from '@ember/routing/router-service'; 11 | 12 | type Transition = Parameters[0]; 13 | type TransitionWithPrivateAPIs = Transition & { 14 | intent?: { 15 | url: string; 16 | }; 17 | }; 18 | 19 | export function withHashSupport(AppRouter: typeof EmberRouter): typeof AppRouter { 20 | return class RouterWithHashSupport extends AppRouter { 21 | constructor(...args: RouterArgs) { 22 | super(...args); 23 | 24 | setupHashSupport(this); 25 | } 26 | }; 27 | } 28 | 29 | export function scrollToHash(hash: string) { 30 | let selector = `[name="${hash}"]`; 31 | let element = document.getElementById(hash) || document.querySelector(selector); 32 | 33 | if (!element) { 34 | warn(`Tried to scroll to element with id or name "${hash}", but it was not found`, { 35 | id: 'no-hash-target', 36 | }); 37 | 38 | return; 39 | } 40 | 41 | /** 42 | * NOTE: the ember router does not support hashes in the URL 43 | * https://github.com/emberjs/rfcs/issues/709 44 | * 45 | * this means that when testing hash changes in the URL, 46 | * we have to assert against the window.location, rather than 47 | * the self-container currentURL helper 48 | * 49 | * NOTE: other ways of changing the URL, but without the smoothness: 50 | * - window[.top].location.replace 51 | */ 52 | 53 | element.scrollIntoView({ behavior: 'smooth' }); 54 | 55 | if (hash !== window.location.hash) { 56 | let withoutHash = location.href.split('#')[0]; 57 | let nextUrl = `${withoutHash}#${hash}`; 58 | // most browsers ignore the title param of pushState 59 | let titleWithoutHash = document.title.split(' | #')[0]; 60 | let nextTitle = `${titleWithoutHash} | #${hash}`; 61 | 62 | history.pushState({}, nextTitle, nextUrl); 63 | document.title = nextTitle; 64 | } 65 | } 66 | 67 | function isLoadingRoute(routeName: string) { 68 | return routeName.endsWith('_loading') || routeName.endsWith('.loading'); 69 | } 70 | 71 | async function setupHashSupport(router: EmberRouter) { 72 | let initialURL: string | undefined; 73 | let owner = getOwner(router) as ApplicationInstance; 74 | 75 | await new Promise((resolve) => { 76 | let interval = setInterval(() => { 77 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 78 | let { currentURL, currentRouteName } = router as any; /* Private API */ 79 | 80 | if (currentURL && !isLoadingRoute(currentRouteName)) { 81 | clearInterval(interval); 82 | initialURL = currentURL; 83 | resolve(null); 84 | } 85 | }, 100); 86 | }); 87 | 88 | if (isDestroyed(owner) || isDestroying(owner)) { 89 | return; 90 | } 91 | 92 | /** 93 | * This handles the initial Page Load, which is not imperceptible through 94 | * route{Did,Will}Change 95 | * 96 | */ 97 | requestAnimationFrame(() => { 98 | eventuallyTryScrollingTo(owner, initialURL); 99 | }); 100 | 101 | let routerService = owner.lookup('service:router') as RouterService; 102 | 103 | function handleHashIntent(transition: TransitionWithPrivateAPIs) { 104 | let { url } = transition.intent || {}; 105 | 106 | if (!url) { 107 | return; 108 | } 109 | 110 | eventuallyTryScrollingTo(owner, url); 111 | } 112 | 113 | routerService.on('routeDidChange', handleHashIntent); 114 | 115 | registerDestructor(router, () => { 116 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 117 | (routerService as any) /* type def missing "off" */ 118 | .off('routeDidChange', handleHashIntent); 119 | }); 120 | } 121 | 122 | const CACHE = new WeakMap(); 123 | 124 | async function eventuallyTryScrollingTo(owner: ApplicationInstance, url?: string) { 125 | // Prevent quick / rapid transitions from continuing to observer beyond their URL-scope 126 | CACHE.get(owner)?.disconnect(); 127 | 128 | if (!url) return; 129 | 130 | let [, hash] = url.split('#'); 131 | 132 | if (!hash) return; 133 | 134 | await waitForPromise(uiSettled(owner)); 135 | 136 | if (isDestroyed(owner) || isDestroying(owner)) { 137 | return; 138 | } 139 | 140 | scrollToHash(hash); 141 | } 142 | 143 | const TIME_SINCE_LAST_MUTATION = 500; // ms 144 | const MAX_TIMEOUT = 2000; // ms 145 | 146 | // exported for testing 147 | export async function uiSettled(owner: ApplicationInstance) { 148 | let timeStarted = new Date().getTime(); 149 | let lastMutationAt = Infinity; 150 | let totalTimeWaited = 0; 151 | 152 | let observer = new MutationObserver(() => { 153 | lastMutationAt = new Date().getTime(); 154 | }); 155 | 156 | CACHE.set(owner, observer); 157 | 158 | observer.observe(document.body, { childList: true, subtree: true }); 159 | 160 | /** 161 | * Wait for DOM mutations to stop until MAX_TIMEOUT 162 | */ 163 | await new Promise((resolve) => { 164 | let frame: number; 165 | 166 | function requestTimeCheck() { 167 | if (frame) cancelAnimationFrame(frame); 168 | 169 | if (isDestroyed(owner) || isDestroying(owner)) { 170 | return; 171 | } 172 | 173 | frame = requestAnimationFrame(() => { 174 | totalTimeWaited = new Date().getTime() - timeStarted; 175 | 176 | let timeSinceLastMutation = new Date().getTime() - lastMutationAt; 177 | 178 | if (totalTimeWaited >= MAX_TIMEOUT) { 179 | return resolve(totalTimeWaited); 180 | } 181 | 182 | if (timeSinceLastMutation >= TIME_SINCE_LAST_MUTATION) { 183 | return resolve(totalTimeWaited); 184 | } 185 | 186 | schedule('afterRender', requestTimeCheck); 187 | }); 188 | } 189 | 190 | schedule('afterRender', requestTimeCheck); 191 | }); 192 | } 193 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/app/.gitkeep -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = async function () { 6 | return { 7 | /** 8 | * Related to https://github.com/emberjs/ember.js/issues/20418 9 | * This prevent npm to break ember-canary & ember-beta : 10 | * ember-resolver@10.0.0 have an optional peerDependency to ember-source ^4.8.3 & it make npm break (where yarn does not). 11 | * Adding this option will prevent npm to complain about this 12 | */ 13 | npmOptions: ['--legacy-peer-deps'], 14 | 15 | scenarios: [ 16 | { 17 | name: 'ember-4.4', 18 | npm: { 19 | devDependencies: { 20 | 'ember-source': '~4.4.0', 21 | }, 22 | }, 23 | }, 24 | { 25 | name: 'ember-4.8', 26 | npm: { 27 | devDependencies: { 28 | 'ember-source': '~4.8.0', 29 | }, 30 | }, 31 | }, 32 | { 33 | name: 'ember-release', 34 | npm: { 35 | devDependencies: { 36 | 'ember-source': await getChannelURL('release'), 37 | }, 38 | }, 39 | }, 40 | { 41 | name: 'ember-beta', 42 | npm: { 43 | devDependencies: { 44 | 'ember-source': await getChannelURL('beta'), 45 | // https://github.com/emberjs/ember.js/issues/20418 46 | 'ember-resolver': '10.0.0', 47 | '@ember/string': '3.0.1', 48 | }, 49 | }, 50 | }, 51 | { 52 | name: 'ember-canary', 53 | npm: { 54 | devDependencies: { 55 | 'ember-source': await getChannelURL('canary'), 56 | // https://github.com/emberjs/ember.js/issues/20418 57 | 'ember-resolver': '10.0.0', 58 | '@ember/string': '3.0.1', 59 | }, 60 | }, 61 | }, 62 | { 63 | name: 'embroider', 64 | npm: { 65 | devDependencies: { 66 | '@embroider/core': '*', 67 | '@embroider/webpack': '*', 68 | '@embroider/compat': '*', 69 | }, 70 | }, 71 | }, 72 | ], 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | if ('@embroider/webpack' in app.dependencies()) { 18 | const { Webpack } = require('@embroider/webpack'); // eslint-disable-line 19 | return require('@embroider/compat') // eslint-disable-line 20 | .compatBuild(app, Webpack, { 21 | staticAddonTestSupportTrees: true, 22 | staticAddonTrees: true, 23 | staticHelpers: true, 24 | staticComponents: true, 25 | }); 26 | } 27 | 28 | return app.toTree(); 29 | }; 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-url-hash-polyfill", 3 | "version": "1.0.11", 4 | "description": "The default blueprint for ember-cli addons.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "repository": "", 9 | "license": "MIT", 10 | "author": "", 11 | "directories": { 12 | "doc": "doc", 13 | "test": "tests" 14 | }, 15 | "scripts": { 16 | "build": "ember build --environment=production", 17 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel 'lint:!(fix)'", 18 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 19 | "lint:hbs": "ember-template-lint .", 20 | "lint:hbs:fix": "ember-template-lint . --fix", 21 | "lint:js": "eslint . --cache", 22 | "lint:js:fix": "eslint . --fix", 23 | "start": "ember serve", 24 | "test": "npm-run-all lint test:*", 25 | "test:ember": "ember test", 26 | "test:ember-compatibility": "ember try:each", 27 | "prepack": "ember ts:precompile", 28 | "postpack": "ember ts:clean" 29 | }, 30 | "dependencies": { 31 | "ember-cli-babel": "^7.26.11", 32 | "ember-cli-htmlbars": "^6.2.0", 33 | "ember-cli-typescript": "^5.2.1", 34 | "ember-test-waiters": "^2.1.3", 35 | "webpack": "5" 36 | }, 37 | "devDependencies": { 38 | "@commitlint/cli": "^13.1.0", 39 | "@commitlint/config-conventional": "^13.1.0", 40 | "@ember/optional-features": "^2.0.0", 41 | "@ember/test-helpers": "^2.9.3", 42 | "@glimmer/component": "^1.0.4", 43 | "@glimmer/tracking": "^1.0.4", 44 | "@nullvoxpopuli/eslint-configs": "^1.3.0", 45 | "@semantic-release/changelog": "^6.0.3", 46 | "@semantic-release/git": "^10.0.1", 47 | "@types/ember-qunit": "^4.0.1", 48 | "@types/ember-resolver": "^5.0.11", 49 | "@types/ember__application": "^3.16.4", 50 | "@types/ember__array": "^3.16.5", 51 | "@types/ember__component": "^3.16.7", 52 | "@types/ember__controller": "^3.16.7", 53 | "@types/ember__debug": "^3.16.7", 54 | "@types/ember__destroyable": "3.22.0", 55 | "@types/ember__engine": "^3.16.4", 56 | "@types/ember__error": "^3.16.2", 57 | "@types/ember__object": "^3.12.7", 58 | "@types/ember__polyfills": "^3.12.2", 59 | "@types/ember__routing": "^3.16.16", 60 | "@types/ember__runloop": "^3.16.4", 61 | "@types/ember__service": "^3.16.2", 62 | "@types/ember__string": "^3.16.3", 63 | "@types/ember__template": "^3.16.2", 64 | "@types/ember__test": "^3.16.2", 65 | "@types/ember__test-helpers": "^2.6.1", 66 | "@types/ember__utils": "^3.16.3", 67 | "@types/htmlbars-inline-precompile": "^1.0.1", 68 | "@types/qunit": "^2.11.3", 69 | "@types/rsvp": "^4.0.4", 70 | "babel-eslint": "^10.1.0", 71 | "broccoli-asset-rev": "^3.0.0", 72 | "ember-auto-import": "^2.6.3", 73 | "ember-cli": "~4.12.1", 74 | "ember-cli-dependency-checker": "^3.3.1", 75 | "ember-cli-inject-live-reload": "^2.1.0", 76 | "ember-cli-sri": "^2.1.1", 77 | "ember-cli-terser": "^4.0.2", 78 | "ember-cli-typescript-blueprints": "^3.0.0", 79 | "ember-disable-prototype-extensions": "^1.1.3", 80 | "ember-load-initializers": "^2.1.2", 81 | "ember-maybe-import-regenerator": "^1.0.0", 82 | "ember-page-title": "^7.0.0", 83 | "ember-qunit": "^5.1.5", 84 | "ember-resolver": "^8.0.3", 85 | "ember-source": "~4.2.0", 86 | "ember-source-channel-url": "^3.0.0", 87 | "ember-template-lint": "^3.2.0", 88 | "ember-try": "^3.0.0-beta.1", 89 | "eslint": "^7.20.0", 90 | "eslint-config-prettier": "^8.3.0", 91 | "eslint-plugin-ember": "^10.2.0", 92 | "eslint-plugin-node": "^11.1.0", 93 | "eslint-plugin-prettier": "^3.3.1", 94 | "loader.js": "^4.7.0", 95 | "npm-run-all": "^4.1.5", 96 | "prettier": "^2.2.1", 97 | "qunit": "^2.18.0", 98 | "qunit-dom": "^2.0.0", 99 | "semantic-release": "^19.0.5", 100 | "typescript": "^4.6.2" 101 | }, 102 | "release": { 103 | "branches": [ 104 | "main", 105 | "master" 106 | ], 107 | "plugins": [ 108 | "@semantic-release/commit-analyzer", 109 | "@semantic-release/release-notes-generator", 110 | "@semantic-release/changelog", 111 | "@semantic-release/npm", 112 | "@semantic-release/github", 113 | "@semantic-release/git" 114 | ] 115 | }, 116 | "volta": { 117 | "node": "16.20.0", 118 | "yarn": "1.22.19" 119 | }, 120 | "engines": { 121 | "node": "16.* || >= 18" 122 | }, 123 | "ember": { 124 | "edition": "octane" 125 | }, 126 | "ember-addon": { 127 | "configPath": "tests/dummy/config" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/app.ts: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | 3 | import config from 'dummy/config/environment'; 4 | import loadInitializers from 'ember-load-initializers'; 5 | import Resolver from 'ember-resolver'; 6 | 7 | export default class App extends Application { 8 | modulePrefix = config.modulePrefix; 9 | podModulePrefix = config.podModulePrefix; 10 | Resolver = Resolver; 11 | } 12 | 13 | loadInitializers(App, config.modulePrefix); 14 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | export default config; 2 | 3 | /** 4 | * Type declarations for 5 | * import config from 'my-app/config/environment' 6 | */ 7 | declare const config: { 8 | environment: string; 9 | modulePrefix: string; 10 | podModulePrefix: string; 11 | locationType: string; 12 | rootURL: string; 13 | APP: Record; 14 | }; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/router.ts: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | 3 | import config from 'dummy/config/environment'; 4 | import { withHashSupport } from 'ember-url-hash-polyfill'; 5 | 6 | @withHashSupport 7 | export default class Router extends EmberRouter { 8 | location = config.locationType; 9 | rootURL = config.rootURL; 10 | } 11 | 12 | Router.map(function () {}); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Dummy"}} 2 | 3 |

Welcome to Ember

4 | 5 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.25.3", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "ember-addon-automated-ci", 18 | "version": "1.0.2", 19 | "blueprints": [ 20 | { 21 | "name": "ember-addon-automated-ci" 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; 4 | 5 | module.exports = { 6 | browsers, 7 | }; 8 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/integration/-helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ember/no-private-routing-service */ 2 | import { settled } from '@ember/test-helpers'; 3 | 4 | import type { Router } from '@ember/routing'; 5 | import type { TestContext } from 'ember-test-helpers'; 6 | 7 | type MapFunction = Parameters[0]; 8 | 9 | interface SetupRouterOptions { 10 | active?: string | string[]; 11 | map?: MapFunction; 12 | rootURL?: string; 13 | } 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-empty-function 16 | const noop = () => {}; 17 | 18 | /** 19 | * A test helper to define a new router map in the context of a test. 20 | * 21 | * Useful for when an integration test may need to interact with the router service, 22 | * but since you're only rendering a component, routing isn't enabled (pre Ember 3.25). 23 | * 24 | * Also useful for testing custom link components. 25 | * 26 | * @example 27 | * 28 | * import { setupRouter } from '@crowdstrike/test-helpers'; 29 | * 30 | * module('tests that need a router', function(hooks) { 31 | * setupRouter(hooks, { 32 | * active: ['some-route-path.foo', 2], 33 | * map: function() { 34 | * this.route('some-route-path', function() { 35 | * this.route('hi'); 36 | * this.route('foo', { path: ':dynamic_segment' }); 37 | * }); 38 | * }, 39 | * }); 40 | * }) 41 | * 42 | * 43 | * @param {NestedHooks} hooks 44 | * @param {Object} configuration - router configuration, as it would be defined in router.js 45 | * @param {Array} [configuration.active] - route segments that make up the active route 46 | * @param {Function} configuration.map - the router map 47 | * @param {string} [configuration.rootURL] - the root URL of the application 48 | */ 49 | export function setupRouter( 50 | hooks: NestedHooks, 51 | { active, map = noop, rootURL = '/' }: SetupRouterOptions = {} 52 | ) { 53 | let originalMaps: unknown[] = []; 54 | 55 | hooks.beforeEach(async function (this: TestContext) { 56 | let router = this.owner.resolveRegistration('router:main'); 57 | 58 | router.rootURL = rootURL; 59 | originalMaps = router.dslCallbacks; 60 | router.dslCallbacks = []; 61 | 62 | router.map(map); 63 | this.owner.lookup('router:main').setupRouter(); 64 | 65 | if (active) { 66 | let routerService = this.owner.lookup('service:router'); 67 | 68 | routerService.transitionTo(...ensureArray(active)); 69 | await settled(); 70 | } 71 | }); 72 | 73 | hooks.afterEach(function (this: TestContext) { 74 | let router = this.owner.resolveRegistration('router:main'); 75 | 76 | router.dslCallbacks = originalMaps; 77 | }); 78 | } 79 | 80 | /** 81 | * For setting up the currently configured router in your app 82 | * 83 | */ 84 | export function setupAppRouter(hooks: NestedHooks) { 85 | hooks.beforeEach(function (this: TestContext) { 86 | this.owner.lookup('router:main').setupRouter(); 87 | }); 88 | } 89 | 90 | export function ensureArray(maybeArray?: T | T[]): T[] { 91 | if (Array.isArray(maybeArray)) { 92 | return maybeArray; 93 | } 94 | 95 | if (!maybeArray) { 96 | return []; 97 | } 98 | 99 | return [maybeArray]; 100 | } 101 | -------------------------------------------------------------------------------- /tests/integration/hash-test.ts: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { assert as debugAssert } from '@ember/debug'; 3 | import Route from '@ember/routing/route'; 4 | import { inject as service } from '@ember/service'; 5 | import { click, find, settled, visit, waitUntil } from '@ember/test-helpers'; 6 | import { hbs } from 'ember-cli-htmlbars'; 7 | import { module, test } from 'qunit'; 8 | import { setupApplicationTest } from 'ember-qunit'; 9 | 10 | import { scrollToHash, uiSettled } from 'ember-url-hash-polyfill'; 11 | 12 | import { setupRouter } from './-helpers'; 13 | 14 | import type RouterService from '@ember/routing/router-service'; 15 | 16 | module('Hash', function (hooks) { 17 | setupApplicationTest(hooks); 18 | 19 | hooks.beforeEach(() => { 20 | location.hash = ''; 21 | }); 22 | hooks.afterEach(() => { 23 | location.hash = ''; 24 | }); 25 | 26 | // TODO: PR this to qunit-dom as assort.dom(element).isInView(); 27 | // assert.dom().isVisible does not check if the element is within the viewport 28 | function isVisible(element: null | Element, parent: Element) { 29 | if (!element) return false; 30 | 31 | let bounds = element.getBoundingClientRect(); 32 | let parentBounds = parent.getBoundingClientRect(); 33 | 34 | return ( 35 | bounds.top >= parentBounds.top && 36 | bounds.left >= parentBounds.left && 37 | bounds.right <= parentBounds.right && 38 | bounds.bottom <= parentBounds.bottom 39 | ); 40 | } 41 | 42 | module('linking with hashes', function (_hooks) { 43 | test('in-page-links can be scrolled to with native anchors', async function (assert) { 44 | this.owner.register( 45 | 'template:application', 46 | hbs` 47 | first 48 | first 49 | 50 |

first!

51 |
52 | 53 |

second!

54 |
55 | ` 56 | ); 57 | 58 | await visit('/'); 59 | 60 | let container = document.querySelector('#ember-testing-container'); 61 | let first = find('#first'); 62 | let second = find('#second'); 63 | 64 | debugAssert(`Expected all test elements to exist`, container && first && second); 65 | 66 | assert.true(isVisible(first, container), 'first header is visible'); 67 | assert.false(isVisible(second, container), 'second header is not visible'); 68 | assert.equal(location.hash, '', 'initially, has no hash'); 69 | 70 | await click('#second-link'); 71 | 72 | assert.false(isVisible(first, container), 'first header is not visible'); 73 | assert.true(isVisible(second, container), 'second header is visible'); 74 | assert.equal(location.hash, '#second', 'clicked hash appears in URL'); 75 | 76 | await click('#first-link'); 77 | 78 | assert.true(isVisible(first, container), 'first header is visible'); 79 | assert.false(isVisible(second, container), 'second header is not visible'); 80 | assert.equal(location.hash, '#first', 'clicked hash appears in URL'); 81 | }); 82 | 83 | test('in-page-links can be scrolled to with custom links', async function (assert) { 84 | class TestApplication extends Controller { 85 | handleClick = (event: MouseEvent) => { 86 | event.preventDefault(); 87 | 88 | debugAssert( 89 | `Expected event to be from an anchor tag`, 90 | event.target instanceof HTMLAnchorElement 91 | ); 92 | 93 | let [, hash] = event.target.href.split('#'); 94 | 95 | scrollToHash(hash); 96 | }; 97 | } 98 | this.owner.register('controller:application', TestApplication); 99 | 100 | this.owner.register( 101 | 'template:application', 102 | hbs` 103 | first 104 | first 105 | 106 |

first!

107 |
108 | 109 |

second!

110 |
111 | ` 112 | ); 113 | 114 | await visit('/'); 115 | 116 | let container = document.querySelector('#ember-testing-container'); 117 | let first = find('#first'); 118 | let second = find('#second'); 119 | 120 | debugAssert(`Expected all test elements to exist`, container && first && second); 121 | 122 | assert.true(isVisible(first, container), 'first header is visible'); 123 | assert.false(isVisible(second, container), 'second header is not visible'); 124 | assert.equal(location.hash, '', 'initially, has no hash'); 125 | 126 | await click('#second-link'); 127 | await scrollSettled(); 128 | 129 | assert.false(isVisible(first, container), 'first header is not visible'); 130 | assert.true(isVisible(second, container), 'second header is visible'); 131 | assert.equal(location.hash, '#second', 'clicked hash appears in URL'); 132 | 133 | await click('#first-link'); 134 | await scrollSettled(); 135 | 136 | assert.true(isVisible(first, container), 'first header is visible'); 137 | assert.false(isVisible(second, container), 'second header is not visible'); 138 | assert.equal(location.hash, '#first', 'clicked hash appears in URL'); 139 | }); 140 | }); 141 | 142 | module('with transitions', function (hooks) { 143 | setupRouter(hooks, { 144 | map: function () { 145 | this.route('foo'); 146 | this.route('bar'); 147 | }, 148 | }); 149 | 150 | test('transitioning only via query params does not break things', async function (assert) { 151 | class TestApplication extends Controller { 152 | queryParams = ['test']; 153 | test = false; 154 | } 155 | class Index extends Controller { 156 | @service declare router: RouterService; 157 | } 158 | 159 | this.owner.register('controller:application', TestApplication); 160 | this.owner.register('controller:index', Index); 161 | this.owner.register( 162 | 'template:application', 163 | hbs` 164 | foo 165 | default 166 | {{outlet}} 167 | ` 168 | ); 169 | this.owner.register( 170 | 'template:index', 171 | hbs` 172 | 173 | qp: {{this.router.currentRoute.queryParams.test}} 174 | 175 | ` 176 | ); 177 | 178 | let router = this.owner.lookup('service:router'); 179 | 180 | await visit('/'); 181 | assert.dom('out').hasText('qp:'); 182 | 183 | await click('#foo'); 184 | assert.dom('out').hasText('qp: foo'); 185 | 186 | await click('#default'); 187 | assert.dom('out').hasText('qp:'); 188 | 189 | router.transitionTo({ queryParams: { test: 'foo' } }); 190 | await settled(); 191 | assert.dom('out').hasText('qp: foo'); 192 | 193 | router.transitionTo({ queryParams: { test: false } }); 194 | await settled(); 195 | assert.dom('out').hasText('qp: false'); 196 | }); 197 | 198 | test('cross-page-Llinks are properly scrolled to', async function (assert) { 199 | this.owner.register( 200 | 'template:foo', 201 | hbs` 202 |

first!

203 |
204 | 205 |

second!

206 |
207 | ` 208 | ); 209 | 210 | this.owner.register( 211 | 'template:bar', 212 | hbs` 213 |

first!

214 |
215 | 216 |

second!

217 |
218 | ` 219 | ); 220 | 221 | let router = this.owner.lookup('service:router'); 222 | let container = document.querySelector('#ember-testing-container'); 223 | 224 | debugAssert(`Expected all test elements to exist`, container); 225 | 226 | router.transitionTo('/foo'); 227 | await uiSettled(this.owner); 228 | 229 | assert.true(isVisible(find('#foo-first'), container), 'first header is visible'); 230 | assert.false(isVisible(find('#foo-second'), container), 'second header is not visible'); 231 | assert.equal(location.hash, '', 'initially, has no hash'); 232 | 233 | router.transitionTo('/bar#bar-second'); 234 | await uiSettled(this.owner); 235 | await scrollSettled(); 236 | 237 | assert.false(isVisible(find('#bar-first'), container), 'first header is not visible'); 238 | assert.true(isVisible(find('#bar-second'), container), 'second header is visible'); 239 | assert.equal(location.hash, '#bar-second', 'clicked hash appears in URL'); 240 | 241 | router.transitionTo('/foo#foo-second'); 242 | await uiSettled(this.owner); 243 | await scrollSettled(); 244 | 245 | assert.false(isVisible(find('#foo-first'), container), 'first header is not visible'); 246 | assert.true(isVisible(find('#foo-second'), container), 'second header is visible'); 247 | assert.equal(location.hash, '#foo-second', 'clicked hash appears in URL'); 248 | }); 249 | }); 250 | 251 | // https://github.com/CrowdStrike/ember-url-hash-polyfill/issues/118 252 | test('transition to route with loading sub state is properly handled', async function (assert) { 253 | this.owner.register( 254 | 'template:application', 255 | hbs` 256 |

first!

257 |
258 | 259 |

second!

260 |
261 | ` 262 | ); 263 | 264 | this.owner.register('template:application-loading', hbs`Loading...`); 265 | 266 | class ApplicationRoute extends Route { 267 | model() { 268 | return new Promise(function (resolve) { 269 | // Keep the timeout > to addon/index.ts "MAX_TIMEOUT" to make this test accurate 270 | setTimeout(resolve, 4000); 271 | }); 272 | } 273 | } 274 | 275 | this.owner.register('route:application', ApplicationRoute); 276 | 277 | await visit('/#second'); 278 | await scrollSettled(); 279 | 280 | let container = document.querySelector('#ember-testing-container'); 281 | let first = find('#first'); 282 | let second = find('#second'); 283 | 284 | debugAssert(`Expected all test elements to exist`, container && first && second); 285 | 286 | await waitUntil(() => isVisible(second, container as Element), { 287 | timeoutMessage: 'second header is visible', 288 | }); 289 | 290 | assert.equal(location.hash, '#second', 'hash appears in URL'); 291 | }); 292 | }); 293 | 294 | export async function scrollSettled() { 295 | // wait for previous stuff to finish 296 | await settled(); 297 | 298 | let timeout = 200; // ms; 299 | let start = new Date().getTime(); 300 | 301 | await Promise.race([ 302 | new Promise((resolve) => setTimeout(resolve, 1000)), 303 | // scrollIntoView does not trigger scroll events 304 | new Promise((resolve) => { 305 | let interval = setInterval(() => { 306 | let now = new Date().getTime(); 307 | 308 | if (now - start >= timeout) { 309 | clearInterval(interval); 310 | 311 | return resolve(now); 312 | } 313 | }, 10); 314 | }), 315 | ]); 316 | 317 | await settled(); 318 | } 319 | -------------------------------------------------------------------------------- /tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import { currentURL, getSettledState, setApplication } from '@ember/test-helpers'; 2 | import { getPendingWaiterState } from '@ember/test-waiters'; 3 | import * as QUnit from 'qunit'; 4 | import { setup } from 'qunit-dom'; 5 | import { start } from 'ember-qunit'; 6 | 7 | import Application from 'dummy/app'; 8 | import config from 'dummy/config/environment'; 9 | 10 | // easy access debugging tools during a paused or stuck test 11 | Object.assign(window, { getSettledState, currentURL, getPendingWaiterState }); 12 | 13 | setApplication(Application.create(config.APP)); 14 | 15 | setup(QUnit.assert); 16 | 17 | start(); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "allowJs": true, 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "alwaysStrict": true, 10 | "strictNullChecks": true, 11 | "strictPropertyInitialization": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noEmitOnError": false, 17 | "noEmit": true, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "baseUrl": ".", 21 | "module": "es6", 22 | "experimentalDecorators": true, 23 | "paths": { 24 | "dummy/tests/*": [ 25 | "tests/*" 26 | ], 27 | "dummy/*": [ 28 | "tests/dummy/app/*", 29 | "app/*" 30 | ], 31 | "ember-url-hash-polyfill": [ 32 | "addon" 33 | ], 34 | "ember-url-hash-polyfill/*": [ 35 | "addon/*" 36 | ], 37 | "ember-url-hash-polyfill/test-support": [ 38 | "addon-test-support" 39 | ], 40 | "ember-url-hash-polyfill/test-support/*": [ 41 | "addon-test-support/*" 42 | ], 43 | "*": [ 44 | "types/*" 45 | ] 46 | } 47 | }, 48 | "include": [ 49 | "app/**/*", 50 | "addon/**/*", 51 | "tests/**/*", 52 | "types/**/*", 53 | "test-support/**/*", 54 | "addon-test-support/**/*" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /types/dummy/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Types for compiled templates 2 | declare module 'ember-url-hash-polyfill/templates/*' { 3 | import { TemplateFactory } from 'htmlbars-inline-precompile'; 4 | const tmpl: TemplateFactory; 5 | export default tmpl; 6 | } 7 | 8 | type RouterArgs = object[]; 9 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdStrike/ember-url-hash-polyfill/d1f44d9655efbaa0700ad2fd8bacc7bb2d0b25f3/vendor/.gitkeep --------------------------------------------------------------------------------