[
  {
    "path": ".browserslistrc",
    "content": "# Browsers that we support\n\nlast 2 Chrome versions\nlast 2 Firefox versions\nlast 2 Safari versions\nlast 2 Edge versions\nie >= 10\n"
  },
  {
    "path": ".codeclimate.yml",
    "content": "version: \"2\"\nchecks:\n  argument-count:\n    config:\n      threshold: 10\n  method-count:\n    config:\n      threshold: 25\n  method-lines:\n    config:\n      threshold: 30\nexclude_patterns:\n  - \"demo/\"\n  - \"dist/\"\n  - \"docs/\"\n  - \"examples/\"\n  - \"jsdoc-template/\"\n  - \"**/node_modules/\"\n  - \"**/test/\"\n  - \"**/tests/\"\n  - \"**/vendor/\"\n  - \"babel.config.js\"\n"
  },
  {
    "path": ".eslintignore",
    "content": "/coverage/\n/dist/\n/docs/\n/examples/\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  parserOptions: {\n    ecmaVersion: 2020,\n    sourceType: 'module'\n  },\n  extends: [\n    'eslint:recommended'\n  ],\n  env: {\n    browser: true,\n    es6: true\n  },\n  rules: {\n    'complexity': ['warn', 6],\n    'max-lines': ['warn', { max: 250, skipBlankLines: true, skipComments: true }],\n    'no-console': 'off',\n    'no-unused-vars': 'off',\n    'prefer-const': 'off'\n  },\n  overrides: [\n    // node files\n    {\n      files: [\n        '.eslintrc.js',\n        'babel.config.js',\n        'jest.config.js',\n        'rollup.config.js',\n        '__mocks__/styleMock.js'\n      ],\n      parserOptions: {\n        sourceType: 'module',\n        ecmaVersion: 2020\n      },\n      env: {\n        node: true\n      }\n    }\n  ]\n};\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: npm\n  directory: \"/\"\n  schedule:\n    interval: weekly\n    time: \"10:00\"\n  open-pull-requests-limit: 10\n  labels:\n  - dependencies\n  versioning-strategy: increase\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "\nname: CI Build\n\non:\n  pull_request: {}\n  push:\n    branches:\n      - master\n    tags:\n      - v*\n\njobs:\n  test:\n    name: Tests\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n      - name: Install Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm i\n      - name: Cache Cypress binary\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/Cypress\n          key: cypress-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}\n          restore-keys: |\n            cypress-${{ runner.os }}-\n      - name: Install Cypress binary\n        run: pnpm exec cypress install\n      - run: pnpm test\n      \n  automerge:\n    needs: [test]\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n      contents: write\n    steps:\n      - uses: fastify/github-action-merge-dependabot@v3.2.0\n        with:\n          github-token: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/plan-release.yml",
    "content": "name: Plan Release\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - master\n  pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/\n    types:\n      - labeled\n      - unlabeled\n\nconcurrency:\n  group: plan-release # only the latest one of these should ever be running\n  cancel-in-progress: true\n\njobs:\n  should-run-release-plan-prepare:\n    name: Should we run release-plan prepare?\n    runs-on: ubuntu-latest\n    outputs:\n      should-prepare: ${{ steps.should-prepare.outputs.should-prepare }}\n    steps:\n      - uses: release-plan/actions/should-prepare-release@v1\n        with:\n          ref: 'master'\n        id: should-prepare\n\n  create-prepare-release-pr:\n    name: Create Prepare Release PR\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    needs: should-run-release-plan-prepare\n    permissions:\n      contents: write\n      issues: read\n      pull-requests: write\n    if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true'    \n    steps:\n      - uses: release-plan/actions/prepare@v1\n        name: Run release-plan prepare\n        with:\n          ref: 'master'\n        env:\n          GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}\n        id: explanation\n\n      - uses: peter-evans/create-pull-request@v7\n        name: Create Prepare Release PR\n        with:\n          commit-message: \"Prepare Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'\"\n          labels: \"internal\"\n          sign-commits: true\n          branch: release-preview\n          title: Prepare Release ${{ steps.explanation.outputs.new-version }}\n          body: |\n            This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍\n\n            -----------------------------------------\n\n            ${{ steps.explanation.outputs.text }}\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "# For every push to the primary branch with .release-plan.json modified,\n# runs release-plan.\n\nname: Publish Stable\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - master\n    paths:\n      - '.release-plan.json'\n\nconcurrency:\n  group: publish-${{ github.head_ref || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  publish:\n    name: \"NPM Publish\"\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      id-token: write\n      attestations: write\n\n    steps:\n      - uses: actions/checkout@v5\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 20\n          registry-url: 'https://registry.npmjs.org'\n          cache: pnpm\n      - run: pnpm install --frozen-lockfile\n      - run: npm install -g npm@latest # ensure that the globally installed npm is new enough to support OIDC\n      - name: Publish to NPM\n        run: NPM_CONFIG_PROVENANCE=true pnpm release-plan publish\n        env:\n          GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Editors\n/.idea/\n/.vscode/\n\n/.log/\n/.nyc_output/\n/coverage/\n/cypress/\n/docs/\n/dist/\n/node_modules/\n/test/unit/dist\n/.DS_Store\n/.sass-cache\n/npm-debug.log*\n/stats.html\n/yarn-error.log\n"
  },
  {
    "path": ".hsdoc",
    "content": "name: \"Tether\"\ndescription: \"Marrying DOM elements for life\"\ndomain: \"tetherjs.dev\"\nsource: \"src/**/*.js\"\nexamples: \"**/*.md\"\nassets: \"{dist/js/*.js,dist/css/*.css,docs/css/*.css,docs/js/*,js,docs/welcome/*,examples/*}\"\n"
  },
  {
    "path": ".npmignore",
    "content": ".idea/\n.vscode/\n\ncoverage/\ncypress/\ndocs/\nexamples/\nesdoc/\njsdoc-template/\ntest/\ntests/\n\n.codeclimate.yml\n.eslintignore\n.eslintrc.js\n.gitignore\n.hsdoc\n.stylelintrc.js\n.travis.yml\nbabel.config.js\ncypress.json\nindex.html\npnpm-lock.yaml\nrollup.config.js\nyarn.lock\nyarn-error.log\n\nCONTRIBUTING.md\nHISTORY.md\n"
  },
  {
    "path": ".npmrc",
    "content": "scripts-prepend-node-path=true\n"
  },
  {
    "path": ".prettierrc.js",
    "content": "'use strict';\n\nmodule.exports = {\n  singleQuote: true,\n  trailingComma: 'none'\n};"
  },
  {
    "path": ".release-plan.json",
    "content": "{\n  \"solution\": {\n    \"tether\": {\n      \"impact\": \"patch\",\n      \"oldVersion\": \"3.0.1\",\n      \"newVersion\": \"3.0.2\",\n      \"tagName\": \"latest\",\n      \"constraints\": [\n        {\n          \"impact\": \"patch\",\n          \"reason\": \"Appears in changelog section :bug: Bug Fix\"\n        }\n      ],\n      \"pkgJSONPath\": \"./package.json\"\n    }\n  },\n  \"description\": \"## Release (2025-12-07)\\n\\n* tether 3.0.2 (patch)\\n\\n#### :bug: Bug Fix\\n* `tether`\\n  * [#1707](https://github.com/shipshapecode/tether/pull/1707) Guard against invalid removeChild ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\\n\\n#### Committers: 1\\n- Robbie Wagner ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\\n\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## Release (2025-12-07)\n\n* tether 3.0.2 (patch)\n\n#### :bug: Bug Fix\n* `tether`\n  * [#1707](https://github.com/shipshapecode/tether/pull/1707) Guard against invalid removeChild ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n\n#### Committers: 1\n- Robbie Wagner ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n\n## Release (2025-12-05)\n\n* tether 3.0.1 (patch)\n\n#### :bug: Bug Fix\n* `tether`\n  * [#1705](https://github.com/shipshapecode/tether/pull/1705) Add prepare to ensure dist is published ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n\n#### Committers: 1\n- Robbie Wagner ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n\n## Release (2025-12-05)\n\n* tether 3.0.0 (major)\n\n#### :boom: Breaking Change\n* `tether`\n  * [#1114](https://github.com/shipshapecode/tether/pull/1114) Drop support for node < 16 ([@monshan](https://github.com/monshan))\n\n#### :rocket: Enhancement\n* `tether`\n  * [#1075](https://github.com/shipshapecode/tether/pull/1075) Remove the markers when tether is destroyed ([@pieter-v](https://github.com/pieter-v))\n\n#### :bug: Bug Fix\n* `tether`\n  * [#835](https://github.com/shipshapecode/tether/pull/835) Fix \"document is not defined\" error ([@diegohaz](https://github.com/diegohaz))\n\n#### :memo: Documentation\n* `tether`\n  * [#1052](https://github.com/shipshapecode/tether/pull/1052) docs: update CDN url ([@drl990114](https://github.com/drl990114))\n\n#### :house: Internal\n* `tether`\n  * [#1703](https://github.com/shipshapecode/tether/pull/1703) Add Release plan ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n  * [#1702](https://github.com/shipshapecode/tether/pull/1702) Switch to pnpm ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n\n#### Committers: 5\n- Haz ([@diegohaz](https://github.com/diegohaz))\n- Marika Shanahan ([@monshan](https://github.com/monshan))\n- Robbie Wagner ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n- [@pieter-v](https://github.com/pieter-v)\n- drl990114 ([@drl990114](https://github.com/drl990114))\n\nDeprecated as of 10.7.0. highlight(lang, code, ...args) has been deprecated.\nDeprecated as of 10.7.0. Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277\n\n## v2.0.0 (2021-03-26)\n\n#### :bug: Bug Fix\n* [#713](https://github.com/shipshapecode/tether/pull/713) Ensure parent still exists when removing event listeners ([@drewjenkins](https://github.com/drewjenkins))\n* [#692](https://github.com/shipshapecode/tether/pull/692) Guard against undefined markers type ([@rwwagner90](https://github.com/rwwagner90))\n\n#### :memo: Documentation\n* [#668](https://github.com/shipshapecode/tether/pull/668) Remove Bootstrap from list ([@MartijnCuppens](https://github.com/MartijnCuppens))\n* [#600](https://github.com/shipshapecode/tether/pull/600) Small Typo Fix ([@SebYLim](https://github.com/SebYLim))\n\n#### Committers: 5\n- Andrew Jenkins ([@drewjenkins](https://github.com/drewjenkins))\n- Martijn Cuppens ([@MartijnCuppens](https://github.com/MartijnCuppens))\n- Robert Wagner ([@rwwagner90](https://github.com/rwwagner90))\n- Sebastian Lim ([@SebYLim](https://github.com/SebYLim))\n- [@dependabot-preview[bot]](https://github.com/apps/dependabot-preview)\n\n## [v2.0.0-beta.5](https://github.com/shipshapecode/tether/tree/v2.0.0-beta.5) (2019-12-05)\n\n[Full Changelog](https://github.com/shipshapecode/tether/compare/v2.0.0-beta.4...v2.0.0-beta.5)\n\n**Implemented enhancements:**\n\n- Allow zeroElement parent to be configurable. [\\#374](https://github.com/shipshapecode/tether/pull/374) ([deanmarano](https://github.com/deanmarano))\n\n**Merged pull requests:**\n\n- Bump rollup from 1.27.5 to 1.27.8 [\\#403](https://github.com/shipshapecode/tether/pull/403) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup-plugin-visualizer from 3.2.2 to 3.3.0 [\\#402](https://github.com/shipshapecode/tether/pull/402) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump autoprefixer from 9.7.2 to 9.7.3 [\\#400](https://github.com/shipshapecode/tether/pull/400) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump cypress from 3.6.1 to 3.7.0 [\\#399](https://github.com/shipshapecode/tether/pull/399) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint from 6.7.1 to 6.7.2 [\\#397](https://github.com/shipshapecode/tether/pull/397) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup-plugin-browsersync from 1.0.0 to 1.1.0 [\\#396](https://github.com/shipshapecode/tether/pull/396) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint-plugin-jest from 23.0.4 to 23.1.1 [\\#395](https://github.com/shipshapecode/tether/pull/395) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint from 6.6.0 to 6.7.1 [\\#394](https://github.com/shipshapecode/tether/pull/394) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump autoprefixer from 9.7.1 to 9.7.2 [\\#393](https://github.com/shipshapecode/tether/pull/393) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/preset-env from 7.7.1 to 7.7.4 [\\#392](https://github.com/shipshapecode/tether/pull/392) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/core from 7.7.2 to 7.7.4 [\\#391](https://github.com/shipshapecode/tether/pull/391) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump postcss from 7.0.21 to 7.0.23 [\\#390](https://github.com/shipshapecode/tether/pull/390) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.27.1 to 1.27.5 [\\#389](https://github.com/shipshapecode/tether/pull/389) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup-plugin-visualizer from 3.1.1 to 3.2.2 [\\#388](https://github.com/shipshapecode/tether/pull/388) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint-plugin-jest from 23.0.3 to 23.0.4 [\\#387](https://github.com/shipshapecode/tether/pull/387) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @testing-library/jest-dom from 4.2.3 to 4.2.4 [\\#386](https://github.com/shipshapecode/tether/pull/386) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup-plugin-visualizer from 2.7.2 to 3.1.1 [\\#385](https://github.com/shipshapecode/tether/pull/385) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.26.5 to 1.27.1 [\\#384](https://github.com/shipshapecode/tether/pull/384) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/preset-env from 7.6.3 to 7.7.1 [\\#383](https://github.com/shipshapecode/tether/pull/383) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup-plugin-visualizer from 2.6.0 to 2.7.2 [\\#382](https://github.com/shipshapecode/tether/pull/382) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.26.0 to 1.26.5 [\\#381](https://github.com/shipshapecode/tether/pull/381) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump cypress from 3.5.0 to 3.6.1 [\\#380](https://github.com/shipshapecode/tether/pull/380) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/core from 7.6.4 to 7.7.2 [\\#379](https://github.com/shipshapecode/tether/pull/379) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @testing-library/jest-dom from 4.2.0 to 4.2.3 [\\#378](https://github.com/shipshapecode/tether/pull/378) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump autoprefixer from 9.7.0 to 9.7.1 [\\#377](https://github.com/shipshapecode/tether/pull/377) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint-plugin-jest from 23.0.0 to 23.0.3 [\\#376](https://github.com/shipshapecode/tether/pull/376) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- test: Add more coverage to the module [\\#375](https://github.com/shipshapecode/tether/pull/375) ([chuckcarpenter](https://github.com/chuckcarpenter))\n\n## [v2.0.0-beta.4](https://github.com/shipshapecode/tether/tree/v2.0.0-beta.4) (2019-10-29)\n\n[Full Changelog](https://github.com/shipshapecode/tether/compare/v2.0.0-beta.3...v2.0.0-beta.4)\n\n**Implemented enhancements:**\n\n- Simplify Evented code [\\#339](https://github.com/shipshapecode/tether/pull/339) ([rwwagner90](https://github.com/rwwagner90))\n\n**Closed issues:**\n\n- Tether is harder to update due to a missing changelog [\\#40](https://github.com/shipshapecode/tether/issues/40)\n\n**Merged pull requests:**\n\n- Bump eslint from 6.5.1 to 6.6.0 [\\#372](https://github.com/shipshapecode/tether/pull/372) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump postcss from 7.0.20 to 7.0.21 [\\#371](https://github.com/shipshapecode/tether/pull/371) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint-plugin-jest from 22.20.0 to 23.0.0 [\\#370](https://github.com/shipshapecode/tether/pull/370) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.25.2 to 1.26.0 [\\#369](https://github.com/shipshapecode/tether/pull/369) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @testing-library/jest-dom from 4.1.2 to 4.2.0 [\\#368](https://github.com/shipshapecode/tether/pull/368) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Split constraints into smaller functions [\\#367](https://github.com/shipshapecode/tether/pull/367) ([rwwagner90](https://github.com/rwwagner90))\n- Add dependabot config [\\#366](https://github.com/shipshapecode/tether/pull/366) ([rwwagner90](https://github.com/rwwagner90))\n- Bump postcss from 7.0.18 to 7.0.20 [\\#365](https://github.com/shipshapecode/tether/pull/365) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump autoprefixer from 9.6.5 to 9.7.0 [\\#364](https://github.com/shipshapecode/tether/pull/364) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump cypress from 3.4.1 to 3.5.0 [\\#363](https://github.com/shipshapecode/tether/pull/363) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.25.1 to 1.25.2 [\\#362](https://github.com/shipshapecode/tether/pull/362) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint-plugin-jest from 22.19.0 to 22.20.0 [\\#361](https://github.com/shipshapecode/tether/pull/361) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.24.0 to 1.25.1 [\\#360](https://github.com/shipshapecode/tether/pull/360) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update getClass to util and add tests [\\#359](https://github.com/shipshapecode/tether/pull/359) ([chuckcarpenter](https://github.com/chuckcarpenter))\n- Bump start-server-and-test from 1.10.5 to 1.10.6 [\\#357](https://github.com/shipshapecode/tether/pull/357) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup-plugin-filesize from 6.2.0 to 6.2.1 [\\#356](https://github.com/shipshapecode/tether/pull/356) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.23.1 to 1.24.0 [\\#355](https://github.com/shipshapecode/tether/pull/355) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump autoprefixer from 9.6.4 to 9.6.5 [\\#354](https://github.com/shipshapecode/tether/pull/354) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint-plugin-jest from 22.17.0 to 22.19.0 [\\#353](https://github.com/shipshapecode/tether/pull/353) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/core from 7.6.3 to 7.6.4 [\\#352](https://github.com/shipshapecode/tether/pull/352) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump start-server-and-test from 1.10.4 to 1.10.5 [\\#350](https://github.com/shipshapecode/tether/pull/350) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/preset-env from 7.6.2 to 7.6.3 [\\#349](https://github.com/shipshapecode/tether/pull/349) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/core from 7.6.2 to 7.6.3 [\\#348](https://github.com/shipshapecode/tether/pull/348) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @testing-library/jest-dom from 4.1.1 to 4.1.2 [\\#347](https://github.com/shipshapecode/tether/pull/347) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @testing-library/jest-dom from 4.1.0 to 4.1.1 [\\#345](https://github.com/shipshapecode/tether/pull/345) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.23.0 to 1.23.1 [\\#344](https://github.com/shipshapecode/tether/pull/344) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump autoprefixer from 9.6.1 to 9.6.4 [\\#343](https://github.com/shipshapecode/tether/pull/343) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.22.0 to 1.23.0 [\\#342](https://github.com/shipshapecode/tether/pull/342) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump start-server-and-test from 1.10.3 to 1.10.4 [\\#341](https://github.com/shipshapecode/tether/pull/341) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump start-server-and-test from 1.10.2 to 1.10.3 [\\#340](https://github.com/shipshapecode/tether/pull/340) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- chore: Add basic landing page for reloading and contribution [\\#338](https://github.com/shipshapecode/tether/pull/338) ([chuckcarpenter](https://github.com/chuckcarpenter))\n- Bump eslint from 6.5.0 to 6.5.1 [\\#337](https://github.com/shipshapecode/tether/pull/337) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump eslint from 6.4.0 to 6.5.0 [\\#336](https://github.com/shipshapecode/tether/pull/336) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump rollup from 1.21.4 to 1.22.0 [\\#335](https://github.com/shipshapecode/tether/pull/335) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Move some bounds utils [\\#334](https://github.com/shipshapecode/tether/pull/334) ([rwwagner90](https://github.com/rwwagner90))\n\n## [v2.0.0-beta.3](https://github.com/shipshapecode/tether/tree/v2.0.0-beta.3) (2019-09-30)\n\n[Full Changelog](https://github.com/shipshapecode/tether/compare/v2.0.0-beta.2...v2.0.0-beta.3)\n\n**Implemented enhancements:**\n\n- Option to disable `position: fixed` [\\#152](https://github.com/shipshapecode/tether/issues/152)\n- Use type-check utils instead of typeof [\\#333](https://github.com/shipshapecode/tether/pull/333) ([rwwagner90](https://github.com/rwwagner90))\n- Move TetherBase and use imports [\\#328](https://github.com/shipshapecode/tether/pull/328) ([rwwagner90](https://github.com/rwwagner90))\n- Split out some utils [\\#325](https://github.com/shipshapecode/tether/pull/325) ([rwwagner90](https://github.com/rwwagner90))\n- More offset utils and tests [\\#319](https://github.com/shipshapecode/tether/pull/319) ([rwwagner90](https://github.com/rwwagner90))\n- Move offset to utils, add tests, test getClass [\\#318](https://github.com/shipshapecode/tether/pull/318) ([rwwagner90](https://github.com/rwwagner90))\n- Refactor rollup config, add tests for pin and out-of-bounds [\\#317](https://github.com/shipshapecode/tether/pull/317) ([rwwagner90](https://github.com/rwwagner90))\n- Move deferred utils to their own file [\\#315](https://github.com/shipshapecode/tether/pull/315) ([rwwagner90](https://github.com/rwwagner90))\n\n**Fixed bugs:**\n\n- Uglify breaks library: \"Super expression must either be null or a function, not undefined\" [\\#298](https://github.com/shipshapecode/tether/issues/298)\n- production build with angular cli \\(uglify\\) results in `undefined` error [\\#295](https://github.com/shipshapecode/tether/issues/295)\n- Does not compile with parcel-bundler [\\#284](https://github.com/shipshapecode/tether/issues/284)\n- Tether not initialize window.Tether when loaded by ReqireJS [\\#257](https://github.com/shipshapecode/tether/issues/257)\n- Can't disable classes [\\#253](https://github.com/shipshapecode/tether/issues/253)\n- Duplicate Identifiers within Tether.js Library [\\#206](https://github.com/shipshapecode/tether/issues/206)\n- Remove classes when set to false [\\#329](https://github.com/shipshapecode/tether/pull/329) ([rwwagner90](https://github.com/rwwagner90))\n\n**Closed issues:**\n\n- Action required: Greenkeeper could not be activated 🚨 [\\#304](https://github.com/shipshapecode/tether/issues/304)\n- Import of Evented from TetherBase.Utils instead of global scope [\\#261](https://github.com/shipshapecode/tether/issues/261)\n- SVGAnimatedString is not defined [\\#201](https://github.com/shipshapecode/tether/issues/201)\n- Option to not append to the body [\\#189](https://github.com/shipshapecode/tether/issues/189)\n- UglifyJS warnings [\\#183](https://github.com/shipshapecode/tether/issues/183)\n- Clean up on destroy [\\#36](https://github.com/shipshapecode/tether/issues/36)\n\n**Merged pull requests:**\n\n- Document events [\\#331](https://github.com/shipshapecode/tether/pull/331) ([rwwagner90](https://github.com/rwwagner90))\n- Add test for fixed anchoring on scroll [\\#330](https://github.com/shipshapecode/tether/pull/330) ([chuckcarpenter](https://github.com/chuckcarpenter))\n- Remove facebook example [\\#327](https://github.com/shipshapecode/tether/pull/327) ([rwwagner90](https://github.com/rwwagner90))\n- chore: Remove example of 3rd party lib [\\#326](https://github.com/shipshapecode/tether/pull/326) ([chuckcarpenter](https://github.com/chuckcarpenter))\n- Bump eslint-plugin-ship-shape from 0.6.0 to 0.7.1 [\\#324](https://github.com/shipshapecode/tether/pull/324) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- test: Remove tooltip example from outside lib [\\#323](https://github.com/shipshapecode/tether/pull/323) ([chuckcarpenter](https://github.com/chuckcarpenter))\n- Bump sinon from 7.4.2 to 7.5.0 [\\#322](https://github.com/shipshapecode/tether/pull/322) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/core from 7.6.0 to 7.6.2 [\\#321](https://github.com/shipshapecode/tether/pull/321) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump @babel/preset-env from 7.6.0 to 7.6.2 [\\#320](https://github.com/shipshapecode/tether/pull/320) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Remove classes on destroy [\\#316](https://github.com/shipshapecode/tether/pull/316) ([rwwagner90](https://github.com/rwwagner90))\n- Bump rollup from 1.21.3 to 1.21.4 [\\#314](https://github.com/shipshapecode/tether/pull/314) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [v2.0.0-beta.2](https://github.com/shipshapecode/tether/tree/v2.0.0-beta.2) (2019-09-18)\n\n[Full Changelog](https://github.com/shipshapecode/tether/compare/v2.0.0-beta.1...v2.0.0-beta.2)\n\n## [v2.0.0-beta.1](https://github.com/shipshapecode/tether/tree/v2.0.0-beta.1) (2019-09-18)\n\n[Full Changelog](https://github.com/shipshapecode/tether/compare/v2.0.0-beta.0...v2.0.0-beta.1)\n\n## [v2.0.0-beta.0](https://github.com/shipshapecode/tether/tree/v2.0.0-beta.0) (2019-09-18)\n\n[Full Changelog](https://github.com/shipshapecode/tether/compare/v1.4.7...v2.0.0-beta.0)\n\n**Breaking changes:**\n\n- Remove dist from git [\\#311](https://github.com/shipshapecode/tether/pull/311) ([rwwagner90](https://github.com/rwwagner90))\n- Move class utils to a utils file, drop IE9 support [\\#310](https://github.com/shipshapecode/tether/pull/310) ([rwwagner90](https://github.com/rwwagner90))\n\n**Implemented enhancements:**\n\n- Return `this` in Evented class for easy chaining [\\#309](https://github.com/shipshapecode/tether/pull/309) ([rwwagner90](https://github.com/rwwagner90))\n- Add `allowPositionFixed` optimization option [\\#308](https://github.com/shipshapecode/tether/pull/308) ([rwwagner90](https://github.com/rwwagner90))\n\n**Closed issues:**\n\n- Transferring ownership [\\#303](https://github.com/shipshapecode/tether/issues/303)\n- No test cases to run the package [\\#293](https://github.com/shipshapecode/tether/issues/293)\n- Not Compatible with TypeScript compiler [\\#263](https://github.com/shipshapecode/tether/issues/263)\n- dist/js/tether.min.js is outdated [\\#256](https://github.com/shipshapecode/tether/issues/256)\n- no version information in min.js [\\#239](https://github.com/shipshapecode/tether/issues/239)\n\n**Merged pull requests:**\n\n- Add tests for enable/disable [\\#306](https://github.com/shipshapecode/tether/pull/306) ([rwwagner90](https://github.com/rwwagner90))\n- Add basic tests, sass -\\> scss, gulp -\\> rollup, etc [\\#305](https://github.com/shipshapecode/tether/pull/305) ([rwwagner90](https://github.com/rwwagner90))\n- Fix code example in README.md [\\#216](https://github.com/shipshapecode/tether/pull/216) ([Stanton](https://github.com/Stanton))\n- Add reactstrap to examples of projects using tether [\\#211](https://github.com/shipshapecode/tether/pull/211) ([eddywashere](https://github.com/eddywashere))\n\n## v1.3.0\n- Tether instances now fire an 'update' event when attachments change due to constraints (#119)\n\n## v1.0.1\n- Update arrow mixin to change arrow pointer event\n\n\n## v1.0.0\n- Coffeescript -> ES6\n- Proper UMD Wrapper\n- Update build steps\n- Add changelog\n- Provide minified CSS\n\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guide\n\nYou will need:\n\n- [pnpm](https://pnpm.io/)\n\nWindows users will need additional setup to enable build capabilities in NPM.\nFrom an administrative command window:\n\n```sh\n    pnpm global add windows-build-tools\n```\n\n## Getting started\n\n1. Fork the project\n2. Clone your forked project by running `git clone git@github.com:{\n   YOUR_USERNAME }/tether.git`\n3. Run `pnpm` to install node modules\n4. Test that you can build the source by running `pnpm build` and ensure the `dist` directory appears.\n\n## Writing code!\n\nWe use `rollup` to facilitate things like transpilation, minification, etc. so\nyou can focus on writing relevant code. If there is a fix or feature you would like\nto contribute, we ask that you take the following steps:\n\n1. Most of the _editable_ code lives in the `src` directory while built code\n   will end up in the `dist` directory upon running `pnpm build`.\n\n2. Some examples are served out of the `examples` directory. Running `pnpm start` will open the list in your browser and initiate a live-reloading session as you make changes.\n\n\n## Opening Pull Requests\n\n1. Please Provide a thoughtful commit message and push your changes to your fork using\n   `git push origin master` (assuming your forked project is using `origin` for\n   the remote name and you are on the `master` branch).\n\n2. Open a Pull Request on GitHub with a description of your changes.\n\n\n## Testing\n\nAll PRs, that change code functionality, are required to have accompanying tests.\n\n### Acceptance Tests\n\nAcceptance tests are run using [`cypress`](https://github.com/cypress-io/cypress). A number of different testing configurations can be found in [`package.json`](/package.json), but you can simply run `pnpm test:ci:watch` to build your latest changes and begin running the tests inside a Chrome browser instance.\n\n⚠️ The acceptance tests are set up to run on `localhost` port `9002`. If you'd like to change this port, make sure to change the `baseUrl` option inside of [`cypress.json`](/cypress.json), and change any references to port `9002` in [`package.json`](/package.json) accordingly.\n\n"
  },
  {
    "path": "HISTORY.md",
    "content": "## v1.3.0\n- Tether instances now fire an 'update' event when attachments change due to constraints (#119)\n\n## v1.0.1\n- Update arrow mixin to change arrow pointer event\n\n\n## v1.0.0\n- Coffeescript -> ES6\n- Proper UMD Wrapper\n- Update build steps\n- Add changelog\n- Provide minified CSS\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2014-2019 HubSpot, Inc.\nCopyright (c) 2019-2022 Ship Shape Consulting LLC\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Tether\n\n<div>\n  <a href=\"https://shipshape.io\">\n    <img align=\"left\" src=\"http://i.imgur.com/DWHQjA5.png\" alt=\"Ship Shape\" width=\"50\" height=\"50\"/>\n  </a>\n \n  **[Tether is maintained by Ship Shape. Contact us for web app consulting, development, and training for your project](https://shipshape.io/services/app-development/)**.\n</div>  \n\n[![npm version](https://badge.fury.io/js/tether.svg)](http://badge.fury.io/js/tether)\n![Download count all time](https://img.shields.io/npm/dt/tether.svg)\n[![npm](https://img.shields.io/npm/dm/tether.svg)]()\n![npm bundle size](https://img.shields.io/bundlephobia/minzip/tether.svg)\n[![CI Build](https://github.com/shipshapecode/tether/actions/workflows/main.yml/badge.svg)](https://github.com/shipshapecode/tether/actions/workflows/main.yml)\n\n## 🐙 Project status 🐙 \n\nWe at Ship Shape have recently taken over Tether's maintenance and hope to modernize and revitalize it. Stay tuned for updates!\n\n## Install\n\n__npm__\n```sh\nnpm install tether\n```\n\nFor the latest beta:\n\n```sh\nnpm install tether@next\n```\n\n__download__\n\nOr download from the [releases](https://github.com/shipshapecode/tether/releases).\n\n## Introduction\n\n[Tether](http://tetherjs.dev/) is a small, focused JavaScript library for defining and managing the position of user interface (UI) elements in relation to one another on a web page. It is a tool for web developers building features that require certain UI elements to be precisely positioned based on the location of another UI element.\n\nThere are often situations in UI development where elements need to be attached to other elements, but placing them right next to each other in the [DOM tree](https://en.wikipedia.org/wiki/Document_Object_Model) can be problematic based on the context. For example, what happens if the element we’re attaching other elements to is fixed to the center of the screen? Or what if the element is inside a scrollable container? How can we prevent the attached element from being clipped as it disappears from view while a user is scrolling? Tether can solve all of these problems and more.\n\nSome common UI elements that have been built with Tether are [tooltips](http://github.hubspot.com/tooltip/docs/welcome), [select menus](http://github.hubspot.com/select/docs/welcome), and [dropdown menus](http://github.hubspot.com/drop/docs/welcome). Tether is flexible and can be used to [solve](http://tetherjs.dev/examples/out-of-bounds/) [all](http://tetherjs.dev/examples/content-visible) [kinds](http://tetherjs.dev/examples/element-scroll) [of](http://tetherjs.dev/examples/enable-disable) interesting [problems](http://tetherjs.dev/examples/viewport); it ensures UI elements stay where they need to be, based on the various user interactions (click, scroll, etc) and layout contexts (fixed positioning, inside scrollable containers, etc).\n\nPlease have a look at the [documentation](http://tetherjs.dev/) for a more detailed explanation of why you might need Tether for your next project.\n\n## What to Use Tether for and When to Use It\n\nTether is a small, focused JavaScript library. For those who might be new to JavaScript, a library is simply a JavaScript file (or files) that contain useful JavaScript code to help achieve tasks easier and faster. Since Tether is a JavaScript user interface (**UI**) library, it contains code to help you to manage the way your website or web app appears.\n\nTether’s goal to is to help you position your elements side-by-side when needed.\n\nLet’s say you’ve started working on your dream project&mdash;a fancy web app that’s sure to become the next big thing! An important feature of your new app is to allow users to comment on shared photos. However, due to limited vertical space and the overall layout of your new app, you’d like to display the comments **next** to the image, similar to how Instagram does it.\n\nYour HTML code might look something like this:\n\n```html\n<div class=\"container\">\n  <img src=\"awesome-picture.jpg\" alt=\"Awesome Picture\" class=\"picture\">\n  <div class=\"comments\">\n    ...\n  </div>\n</div>\n```\n\nNow, you could achieve this with some CSS using its `position` property, but going this route can be problematic since many of `position`’s values take elements **out** of the natural DOM flow. For example, if you have an element at the bottom of your HTML document, using `position: absolute` or `position: fixed` might could move it all the way to the top of your website in the browser.\n\nNot only that, but you also have to make manual adjustments to ensure **other** elements aren’t negatively affected by the positioned elements. Not to mention, you probably want your comment box to be **responsive**, and look good across different device sizes. Coding a solution for this manually is a challenge all on its own.\n\n**Enter Tether!**\n\nAfter installing Tether and including it in your project, you can begin using it!\n\n1. In your JavaScript file, create a new instance (or constructor function) of the `Tether` object:\n\n    ```javascript\n    new Tether({});\n    ```\n\n2. Within the curly braces (`{}`) you can configure the library’s options. Tether’s extensive list of options can be found in the [Tether documentation](http://tetherjs.dev/).\n\n    ```javascript\n    new Tether({\n      element: '.comments',\n      target: '.picture',\n      attachment: 'top right',\n      targetAttachment: 'top left'\n    });\n    ```\n\nNow you have a perfectly placed comment section to go with your awesome picture! It’ll even stay attached to the element when a user resizes their browser window.\n\nThere are tons of other useful features of Tether as well, instead of “comment boxes” you could also build:\n\n* Tooltips for useful hints and tricks,\n* Dropdown menus,\n* Autocomplete popups for forms,\n* and [more](http://tetherjs.dev/examples/list_of_examples/)!\n\n## Usage\nYou only need to include `tether.min.js` in your page:\n```\n<script src=\"path/to/dist/js/tether.min.js\"></script>\n```\nOr use a CDN:\n```\n<script src=\"https://cdn.jsdelivr.net/npm/tether@2.0.0-beta.5/dist/js/tether.min.js\"></script>\n```\n\nThe css files are not required to get tether running.\n\nFor more details jump straight in to the detailed [Usage](http://tetherjs.dev/#usage) page.\n\n[![Tether Docs](http://i.imgur.com/YCx8cLr.png)](http://tetherjs.dev/#usage)\n\n[Demo & API Documentation](http://tetherjs.dev/)\n\n## Contributing\n\nWe encourage contributions of all kinds. If you would like to contribute in some way, please review our [guidelines for contributing](CONTRIBUTING.md).\n\n## License\nCopyright &copy; 2019-2022 Ship Shape Consulting LLC - [MIT License](LICENSE)\nCopyright &copy; 2014-2018 HubSpot - [MIT License](LICENSE)\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Release Process\n\nReleases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged.\n\n## Preparation\n\nSince the majority of the actual release process is automated, the remaining tasks before releasing are:\n\n- correctly labeling **all** pull requests that have been merged since the last release\n- updating pull request titles so they make sense to our users\n\nSome great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall\nguiding principle here is that changelogs are for humans, not machines.\n\nWhen reviewing merged PR's the labels to be used are:\n\n- breaking - Used when the PR is considered a breaking change.\n- enhancement - Used when the PR adds a new feature or enhancement.\n- bug - Used when the PR fixes a bug included in a previous release.\n- documentation - Used when the PR adds or updates documentation.\n- internal - Internal changes or things that don't fit in any other category.\n\n**Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal`\n\n## Release\n\nOnce the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/shipshapecode/tether/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR\n"
  },
  {
    "path": "__mocks__/styleMock.js",
    "content": "module.exports = {};"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = function(api) {\n  api.cache(true);\n\n  return {\n    env: {\n      development: {\n        presets: [\n          [\n            '@babel/preset-env',\n            {\n              loose: true\n            }\n          ]\n        ]\n      },\n      test: {\n        presets: [\n          [\n            '@babel/preset-env'\n          ]\n        ],\n        plugins: [\n          'babel-plugin-rewire',\n          'transform-es2015-modules-commonjs'\n        ]\n      }\n    }\n  };\n};\n"
  },
  {
    "path": "cypress.config.cjs",
    "content": "const { defineConfig } = require('cypress')\n\nmodule.exports = defineConfig({\n  fixturesFolder: 'test/cypress/fixtures',\n  video: false,\n  e2e: {\n    // We've imported your old cypress plugins here.\n    // You may want to clean this up later by importing these.\n    setupNodeEvents(on, config) {\n      return require('./test/cypress/plugins/index.js')(on, config)\n    },\n    baseUrl: 'http://localhost:9002',\n    specPattern: 'test/cypress/integration/**/*.cy.{js,jsx,ts,tsx}',\n    supportFile: 'test/cypress/support/index.js',\n  },\n})\n"
  },
  {
    "path": "examples/common/css/style.css",
    "content": "body {\n    min-height: 3000px;\n}\n.element {\n    width: 200px;\n    height: 200px;\n    background-color: #fe8;\n    position: absolute;\n    z-index: 6;\n}\n\n.target {\n    width: 300px;\n    height: 50px;\n    margin: 0 35%;\n    background-color: #4e9;\n}\n\n.container {\n    height: 600px;\n    overflow: scroll;\n    width: 600px;\n    border: 20px solid #CCC;\n    margin-top: 100px;\n}\n\nbody {\n    padding: 15px;\n}\n\nbody > .container {\n    margin: 0 auto;\n}\n\n.pad {\n    height: 400px;\n    width: 100px;\n}\n\n.instructions {\n    width: 100%;\n    text-align: center;\n    font-size: 24px;\n    padding: 15px;\n    background-color: rgba(210, 180, 140, 0.4);\n    margin: -15px -15px 0 -15px;\n}\n\n"
  },
  {
    "path": "examples/content-visible/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n    </head>\n    <body>\n\n    <div class=\"instructions\">Scroll the page</div>\n\n    <style>\n      .instructions {\n        width: 100%;\n        text-align: center;\n        font-size: 24px;\n        padding: 15px;\n        background-color: rgba(210, 180, 140, 0.4);\n      }\n\n      * {\n        box-sizing: border-box;\n      }\n      body {\n        min-height: 1200vh;\n        height: 100%;\n      }\n\n      .content-box {\n        width: 600px;\n        border: 10px solid #999;\n        height: 600vh;\n        background-color: #439CCC;\n        margin: 200vh auto;\n      }\n      .element {\n        border: 10px solid #999;\n        background-color: #FFDC00;\n        width: 300px;\n        height: 200px;\n        padding: 0 15px;\n        font-size: 20px;\n        font-weight: bold;\n      }\n    </style>\n\n    <div class=\"content-box\">\n      <div class=\"element\">\n        <p>This is some sort of crazy dialog.</p>\n\n        <p>It's setup to align with the center of the visible part of the blue area.</p>\n      </div>\n    </div>\n\n    <script src=\"../../dist/js/tether.js\"></script>\n    <script>\n      new Tether({\n        element: '.element',\n        target: '.content-box',\n        attachment: 'middle center',\n        targetAttachment: 'middle center',\n        targetModifier: 'visible'\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/dolls/dolls.css",
    "content": ".tether-element, .tether-target {\n  width: 200px;\n  height: 50px;\n  background-color: #4cc;\n  position: absolute;\n}\nbody {\n  width: 100%;\n  height: 100%;\n  overflow: scroll;\n}\n.scroll {\n  width: 400%;\n  height: 400%;\n}\n.tether-target:not(.tether-element) {\n  cursor: move;\n}\n"
  },
  {
    "path": "examples/dolls/dolls.js",
    "content": "var tethers = [];\n\ndocument.addEventListener('DOMContentLoaded', function(){\n  dragging = null;\n\n  document.body.addEventListener('mouseup', function(){\n    dragging = null;\n  });\n\n  document.body.addEventListener('mousemove', function(e){\n    if (dragging){\n      dragging.style.top = e.clientY + 'px';\n      dragging.style.left = e.clientX + 'px';\n\n      Tether.position()\n    }\n  });\n\n  document.body.addEventListener('mousedown', function(e){\n    if (e.target.getAttribute('data-index'))\n      dragging = e.target;\n  })\n\n  var count = 60;\n  var parent = null;\n  var dir = 'left';\n  var first = null;\n\n  while (count--){\n    var el = document.createElement('div');\n    el.setAttribute('data-index', count);\n    document.querySelector('.scroll').appendChild(el);\n\n    if (!first)\n      first = el;\n \n    if (count % 10 === 0)\n      dir = dir == 'right' ? 'left' : 'right';\n\n    if (parent){\n      tethers.push(new Tether({\n        element: el,\n        target: parent,\n        attachment: 'middle ' + dir,\n        targetOffset: (dir == 'left' ? '10px 10px' : '10px -10px')\n      }));\n\n    }\n\n    parent = el;\n  }\n\n  initAnim(first);\n});\n\nfunction initAnim(el){\n  var start = performance.now()\n  var last = 0;\n  var lastTop = 0;\n  var tick = function(){\n    var diff = performance.now() - last;\n\n    if (!last || diff > 50){\n      last = performance.now();\n\n      var nextTop = 50 * Math.sin((last - start) / 1000);\n\n      var curTop = parseFloat(el.style.top || 0);\n      var topChange = nextTop - lastTop;\n      lastTop = nextTop;\n\n      var top = curTop + topChange;\n\n      el.style.top = top + 'px';\n\n      Tether.position();\n    }\n\n    requestAnimationFrame(tick);\n  };\n\n  tick();\n}\n"
  },
  {
    "path": "examples/dolls/index.html",
    "content": "<link rel=\"stylesheet\" href=\"./dolls.css\" />\n<script src=\"../../dist/js/tether.js\"></script>\n<script src=\"./dolls.js\"></script>\n<body>\n  <div class=\"scroll\">\n  </div>\n</body>\n"
  },
  {
    "path": "examples/element-scroll/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n      <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n    </head>\n    <body>\n\n    <div class=\"scroll\">\n      <p>For a long time after the course of the steamer <em>Sofala</em> had been\n      altered for the land, the low swampy coast had retained its appearance\n      of a mere smudge of darkness beyond a belt of glitter. The sunrays\n      seemed to fall violently upon the calm sea--seemed to shatter themselves\n      upon an adamantine surface into sparkling dust, into a dazzling vapor\n      of light that blinded the eye and wearied the brain with its unsteady\n      brightness.</p>\n\n      <p>Captain Whalley did not look at it. When his Serang, approaching the\n      roomy cane arm-chair which he filled capably, had informed him in a low\n      voice that the course was to be altered, he had risen at once and had\n      remained on his feet, face forward, while the head of his ship swung\n      through a quarter of a circle. He had not uttered a single word, not\n      even the word to steady the helm. It was the Serang, an elderly, alert,\n      little Malay, with a very dark skin, who murmured the order to the\n      helmsman. And then slowly Captain Whalley sat down again in the\n      arm-chair on the bridge and fixed his eyes on the deck between his feet.</p>\n\n      <p>He could not hope to see anything new upon this lane of the sea. He had\n      been on these coasts for the last three years. From Low Cape to Malantan\n      the distance was fifty miles, six hours' steaming for the old ship with\n      the tide, or seven against. Then you steered straight for the land, and\n      by-and-by three palms would appear on the sky, tall and slim, and with\n      their disheveled heads in a bunch, as if in confidential criticism of\n      the dark mangroves. The Sofala would be headed towards the somber\n      strip of the coast, which at a given moment, as the ship closed with\n      it obliquely, would show several clean shining fractures--the brimful\n      estuary of a river. Then on through a brown liquid, three parts water\n      and one part black earth, on and on between the low shores, three parts\n      black earth and one part brackish water, the Sofala would plow her way\n      up-stream, as she had done once every month for these seven years or\n      more, long before he was aware of her existence, long before he had ever\n      thought of having anything to do with her and her invariable voyages.\n      The old ship ought to have known the road better than her men, who had\n      not been kept so long at it without a change; better than the faithful\n      Serang, whom he had brought over from his last ship to keep the\n      captain's watch; better than he himself, who had been her captain for\n      the last three years only. She could always be depended upon to make her\n      courses. Her compasses were never out. She was no trouble at all to\n      take about, as if her great age had given her knowledge, wisdom, and\n      steadiness. She made her landfalls to a degree of the bearing, and\n      almost to a minute of her allowed time. At any moment, as he sat on\n      the bridge without looking up, or lay sleepless in his bed, simply by\n      reckoning the days and the hours he could tell where he was--the precise\n      spot of the beat. He knew it well too, this monotonous huckster's\n      round, up and down the Straits; he knew its order and its sights and its\n      people. Malacca to begin with, in at daylight and out at dusk, to cross\n      over with a rigid phosphorescent wake this highway of the Far East.\n      Darkness and gleams on the water, clear stars on a black sky, perhaps\n      the lights of a home steamer keeping her unswerving course in the\n      middle, or maybe the elusive shadow of a native craft with her mat sails\n      flitting by silently--and the low land on the other side in sight\n      at daylight. At noon the three palms of the next place of call, up a\n      sluggish river. The only white man residing there was a retired young\n      sailor, with whom he had become friendly in the course of many voyages.\n      Sixty miles farther on there was another place of call, a deep bay with\n      only a couple of houses on the beach. And so on, in and out, picking\n      up coastwise cargo here and there, and finishing with a hundred miles'\n      steady steaming through the maze of an archipelago of small islands up\n      to a large native town at the end of the beat. There was a three days'\n      rest for the old ship before he started her again in inverse order,\n      seeing the same shores from another bearing, hearing the same voices\n      in the same places, back again to the Sofala's port of registry on\n      the great highway to the East, where he would take up a berth nearly\n      opposite the big stone pile of the harbor office till it was time to\n      start again on the old round of 1600 miles and thirty days. Not a very\n      enterprising life, this, for Captain Whalley, Henry Whalley, otherwise\n      Dare-devil Harry--Whalley of the Condor, a famous clipper in her day.\n      No. Not a very enterprising life for a man who had served famous firms,\n      who had sailed famous ships (more than one or two of them his own); who\n      had made famous passages, had been the pioneer of new routes and new\n      trades; who had steered across the unsurveyed tracts of the South Seas,\n      and had seen the sun rise on uncharted islands. Fifty years at sea, and\n      forty out in the East (\"a pretty thorough apprenticeship,\" he used\n      to remark smilingly), had made him honorably known to a generation of\n      shipowners and merchants in all the ports from Bombay clear over to\n      where the East merges into the West upon the coast of the two Americas.\n      His fame remained writ, not very large but plain enough, on the\n      Admiralty charts. Was there not somewhere between Australia and China a\n      Whalley Island and a Condor Reef? On that dangerous coral formation the\n      celebrated clipper had hung stranded for three days, her captain and\n      crew throwing her cargo overboard with one hand and with the other, as\n      it were, keeping off her a flotilla of savage war-canoes. At that time\n      neither the island nor the reef had any official existence. Later the\n      officers of her Majesty's steam vessel Fusilier, dispatched to make a\n      survey of the route, recognized in the adoption of these two names the\n      enterprise of the man and the solidity of the ship. Besides, as anyone\n      who cares may see, the \"General Directory,\" vol. ii. p. 410, begins the\n      description of the \"Malotu or Whalley Passage\" with the words: \"This\n      advantageous route, first discovered in 1850 by Captain Whalley in the\n      ship Condor,\" &amp;c., and ends by recommending it warmly to sailing vessels\n      leaving the China ports for the south in the months from December to\n      April inclusive.</p>\n\n      <p>This was the clearest gain he had out of life. Nothing could rob him\n      of this kind of fame. The piercing of the Isthmus of Suez, like the\n      breaking of a dam, had let in upon the East a flood of new ships, new\n      men, new methods of trade. It had changed the face of the Eastern seas\n      and the very spirit of their life; so that his early experiences meant\n      nothing whatever to the new generation of seamen.</p>\n\n      <p>In those bygone days he had handled many thousands of pounds of his\n      employers' money and of his own; he had attended faithfully, as by law\n      a shipmaster is expected to do, to the conflicting interests of owners,\n      charterers, and underwriters. He had never lost a ship or consented to\n      a shady transaction; and he had lasted well, outlasting in the end the\n      conditions that had gone to the making of his name. He had buried his\n      wife (in the Gulf of Petchili), had married off his daughter to the man\n      of her unlucky choice, and had lost more than an ample competence in the\n      crash of the notorious Travancore and Deccan Banking Corporation, whose\n      downfall had shaken the East like an earthquake. And he was sixty-five\n      years old.</p>\n\n      <p>His age sat lightly enough on him; and of his ruin he was not ashamed.\n      He had not been alone to believe in the stability of the Banking\n      Corporation. Men whose judgment in matters of finance was as expert as\n      his seamanship had commended the prudence of his investments, and had\n      themselves lost much money in the great failure. The only difference\n      between him and them was that he had lost his all. And yet not his all.\n      There had remained to him from his lost fortune a very pretty little\n      bark, Fair Maid, which he had bought to occupy his leisure of a retired\n      sailor--\"to play with,\" as he expressed it himself.</p>\n\n      <p>He had formally declared himself tired of the sea the year preceding his\n      daughter's marriage. But after the young couple had gone to settle in\n      Melbourne he found out that he could not make himself happy on shore. He\n      was too much of a merchant sea-captain for mere yachting to satisfy him.\n      He wanted the illusion of affairs; and his acquisition of the Fair\n      Maid preserved the continuity of his life. He introduced her to his\n      acquaintances in various ports as \"my last command.\" When he grew too\n      old to be trusted with a ship, he would lay her up and go ashore to be\n      buried, leaving directions in his will to have the bark towed out and\n      scuttled decently in deep water on the day of the funeral. His daughter\n      would not grudge him the satisfaction of knowing that no stranger would\n      handle his last command after him. With the fortune he was able to leave\n      her, the value of a 500-ton bark was neither here nor there. All this\n      would be said with a jocular twinkle in his eye: the vigorous old man\n      had too much vitality for the sentimentalism of regret; and a little\n      wistfully withal, because he was at home in life, taking a genuine\n      pleasure in its feelings and its possessions; in the dignity of his\n      reputation and his wealth, in his love for his daughter, and in his\n      satisfaction with the ship--the plaything of his lonely leisure.</p>\n\n      <p>He had the cabin arranged in accordance with his simple ideal of comfort\n      at sea. A big bookcase (he was a great reader) occupied one side of his\n      stateroom; the portrait of his late wife, a flat bituminous oil-painting\n      representing the profile and one long black ringlet of a young woman,\n      faced his bed-place. Three chronometers ticked him to sleep and greeted\n      him on waking with the tiny competition of their beats. He rose at five\n      every day. The officer of the morning watch, drinking his early cup\n      of coffee aft by the wheel, would hear through the wide orifice of the\n      copper ventilators all the splashings, blowings, and splutterings of\n      his captain's toilet. These noises would be followed by a sustained\n      deep murmur of the Lord's Prayer recited in a loud earnest voice. Five\n      minutes afterwards the head and shoulders of Captain Whalley emerged\n      out of the companion-hatchway. Invariably he paused for a while on the\n      stairs, looking all round at the horizon; upwards at the trim of the\n      sails; inhaling deep draughts of the fresh air. Only then he would step\n      out on the poop, acknowledging the hand raised to the peak of the cap\n      with a majestic and benign \"Good morning to you.\" He walked the deck\n      till eight scrupulously. Sometimes, not above twice a year, he had to\n      use a thick cudgel-like stick on account of a stiffness in the hip--a\n      slight touch of rheumatism, he supposed. Otherwise he knew nothing of\n      the ills of the flesh. At the ringing of the breakfast bell he went\n      below to feed his canaries, wind up the chronometers, and take the\n      head of the table. From there he had before his eyes the big carbon\n      photographs of his daughter, her husband, and two fat-legged babies\n      --his grandchildren--set in black frames into the maplewood bulkheads\n      of the cuddy. After breakfast he dusted the glass over these portraits\n      himself with a cloth, and brushed the oil painting of his wife with a\n      plumate kept suspended from a small brass hook by the side of the heavy\n      gold frame. Then with the door of his stateroom shut, he would sit down\n      on the couch under the portrait to read a chapter out of a thick pocket\n      Bible--her Bible. But on some days he only sat there for half an hour\n      with his finger between the leaves and the closed book resting on his\n      knees. Perhaps he had remembered suddenly how fond of boat-sailing she\n      used to be.</p>\n\n      <p>She had been a real shipmate and a true woman too. It was like an\n      article of faith with him that there never had been, and never could be,\n      a brighter, cheerier home anywhere afloat or ashore than his home under\n      the poop-deck of the Condor, with the big main cabin all white and gold,\n      garlanded as if for a perpetual festival with an unfading wreath. She\n      had decorated the center of every panel with a cluster of home flowers.\n      It took her a twelvemonth to go round the cuddy with this labor of love.\n      To him it had remained a marvel of painting, the highest achievement of\n      taste and skill; and as to old Swinburne, his mate, every time he\n      came down to his meals he stood transfixed with admiration before the\n      progress of the work. You could almost smell these roses, he declared,\n      sniffing the faint flavor of turpentine which at that time pervaded the\n      saloon, and (as he confessed afterwards) made him somewhat less hearty\n      than usual in tackling his food. But there was nothing of the sort to\n      interfere with his enjoyment of her singing. \"Mrs. Whalley is a regular\n      out-and-out nightingale, sir,\" he would pronounce with a judicial air\n      after listening profoundly over the skylight to the very end of the\n      piece. In fine weather, in the second dog-watch, the two men could hear\n      her trills and roulades going on to the accompaniment of the piano in\n      the cabin. On the very day they got engaged he had written to London\n      for the instrument; but they had been married for over a year before it\n      reached them, coming out round the Cape. The big case made part of the\n      first direct general cargo landed in Hong-kong harbor--an event that to\n      the men who walked the busy quays of to-day seemed as hazily remote as\n      the dark ages of history. But Captain Whalley could in a half hour of\n      solitude live again all his life, with its romance, its idyl, and its\n      sorrow. He had to close her eyes himself. She went away from under the\n      ensign like a sailor's wife, a sailor herself at heart. He had read\n      the service over her, out of her own prayer-book, without a break in his\n      voice. When he raised his eyes he could see old Swinburne facing him\n      with his cap pressed to his breast, and his rugged, weather-beaten,\n      impassive face streaming with drops of water like a lump of chipped red\n      granite in a shower. It was all very well for that old sea-dog to cry.\n      He had to read on to the end; but after the splash he did not remember\n      much of what happened for the next few days. An elderly sailor of the\n      crew, deft at needlework, put together a mourning frock for the child\n      out of one of her black skirts.</p>\n\n      <p>He was not likely to forget; but you cannot dam up life like a sluggish\n      stream. It will break out and flow over a man's troubles, it will close\n      upon a sorrow like the sea upon a dead body, no matter how much love has\n      gone to the bottom. And the world is not bad. People had been very\n      kind to him; especially Mrs. Gardner, the wife of the senior partner\n      in Gardner, Patteson, &amp; Co., the owners of the Condor. It was she who\n      volunteered to look after the little one, and in due course took her to\n      England (something of a journey in those days, even by the overland\n      mail route) with her own girls to finish her education. It was ten years\n      before he saw her again.</p>\n\n      <p>As a little child she had never been frightened of bad weather; she\n      would beg to be taken up on deck in the bosom of his oilskin coat to\n      watch the big seas hurling themselves upon the Condor. The swirl and\n      crash of the waves seemed to fill her small soul with a breathless\n      delight. \"A good boy spoiled,\" he used to say of her in joke. He had\n      named her Ivy because of the sound of the word, and obscurely fascinated\n      by a vague association of ideas. She had twined herself tightly round\n      his heart, and he intended her to cling close to her father as to a\n      tower of strength; forgetting, while she was little, that in the nature\n      of things she would probably elect to cling to someone else. But\n      he loved life well enough for even that event to give him a certain\n      satisfaction, apart from his more intimate feeling of loss.</p>\n\n      <p>After he had purchased the Fair Maid to occupy his loneliness, he\n      hastened to accept a rather unprofitable freight to Australia simply for\n      the opportunity of seeing his daughter in her own home. What made him\n      dissatisfied there was not to see that she clung now to somebody else,\n      but that the prop she had selected seemed on closer examination \"a\n      rather poor stick\"--even in the matter of health. He disliked his\n      son-in-law's studied civility perhaps more than his method of\n      handling the sum of money he had given Ivy at her marriage. But of his\n      apprehensions he said nothing. Only on the day of his departure, with\n      the hall-door open already, holding her hands and looking steadily into\n      her eyes, he had said, \"You know, my dear, all I have is for you and the\n      chicks. Mind you write to me openly.\" She had answered him by an almost\n      imperceptible movement of her head. She resembled her mother in\n      the color of her eyes, and in character--and also in this, that she\n      understood him without many words.</p>\n\n      <p>Sure enough she had to write; and some of these letters made Captain\n      Whalley lift his white eye-brows. For the rest he considered he was\n      reaping the true reward of his life by being thus able to produce on\n      demand whatever was needed. He had not enjoyed himself so much in a\n      way since his wife had died. Characteristically enough his son-in-law's\n      punctuality in failure caused him at a distance to feel a sort of\n      kindness towards the man. The fellow was so perpetually being jammed on\n      a lee shore that to charge it all to his reckless navigation would be\n      manifestly unfair. No, no! He knew well what that meant. It was bad\n      luck. His own had been simply marvelous, but he had seen in his life too\n      many good men--seamen and others--go under with the sheer weight of bad\n      luck not to recognize the fatal signs. For all that, he was cogitating\n      on the best way of tying up very strictly every penny he had to leave,\n      when, with a preliminary rumble of rumors (whose first sound reached\n      him in Shanghai as it happened), the shock of the big failure came;\n      and, after passing through the phases of stupor, of incredulity, of\n      indignation, he had to accept the fact that he had nothing to speak of\n      to leave.</p>\n\n      <p>Upon that, as if he had only waited for this catastrophe, the unlucky\n      man, away there in Melbourne, gave up his unprofitable game, and sat\n      down--in an invalid's bath-chair at that too. \"He will never walk\n      again,\" wrote the wife. For the first time in his life Captain Whalley\n      was a bit staggered.</p>\n\n      <p>The Fair Maid had to go to work in bitter earnest now. It was no longer\n      a matter of preserving alive the memory of Dare-devil Harry Whalley in\n      the Eastern Seas, or of keeping an old man in pocket-money and clothes,\n      with, perhaps, a bill for a few hundred first-class cigars thrown in at\n      the end of the year. He would have to buckle-to, and keep her going hard\n      on a scant allowance of gilt for the ginger-bread scrolls at her stem\n      and stern.</p>\n\n      <p>This necessity opened his eyes to the fundamental changes of the world.\n      Of his past only the familiar names remained, here and there, but\n      the things and the men, as he had known them, were gone. The name of\n      Gardner, Patteson, &amp; Co. was still displayed on the walls of warehouses\n      by the waterside, on the brass plates and window-panes in the business\n      quarters of more than one Eastern port, but there was no longer a\n      Gardner or a Patteson in the firm. There was no longer for Captain\n      Whalley an arm-chair and a welcome in the private office, with a bit of\n      business ready to be put in the way of an old friend, for the sake of\n      bygone services. The husbands of the Gardner girls sat behind the desks\n      in that room where, long after he had left the employ, he had kept his\n      right of entrance in the old man's time. Their ships now had yellow\n      funnels with black tops, and a time-table of appointed routes like a\n      confounded service of tramways. The winds of December and June were all\n      one to them; their captains (excellent young men he doubted not) were,\n      to be sure, familiar with Whalley Island, because of late years the\n      Government had established a white fixed light on the north end (with\n      a red danger sector over the Condor Reef), but most of them would have\n      been extremely surprised to hear that a flesh-and-blood Whalley still\n      existed--an old man going about the world trying to pick up a cargo here\n      and there for his little bark.</p>\n\n      <p>And everywhere it was the same. Departed the men who would have nodded\n      appreciatively at the mention of his name, and would have thought\n      themselves bound in honor to do something for Dare-devil Harry Whalley.\n      Departed the opportunities which he would have known how to seize; and\n      gone with them the white-winged flock of clippers that lived in the\n      boisterous uncertain life of the winds, skimming big fortunes out of\n      the foam of the sea. In a world that pared down the profits to an\n      irreducible minimum, in a world that was able to count its disengaged\n      tonnage twice over every day, and in which lean charters were snapped up\n      by cable three months in advance, there were no chances of fortune for\n      an individual wandering haphazard with a little bark--hardly indeed any\n      room to exist.</p>\n\n      <p>He found it more difficult from year to year. He suffered greatly from\n      the smallness of remittances he was able to send his daughter. Meantime\n      he had given up good cigars, and even in the matter of inferior cheroots\n      limited himself to six a day. He never told her of his difficulties, and\n      she never enlarged upon her struggle to live. Their confidence in each\n      other needed no explanations, and their perfect understanding endured\n      without protestations of gratitude or regret. He would have been shocked\n      if she had taken it into her head to thank him in so many words, but\n      he found it perfectly natural that she should tell him she needed two\n      hundred pounds.</p>\n\n      <p>He had come in with the Fair Maid in ballast to look for a freight in\n      the Sofala's port of registry, and her letter met him there. Its tenor\n      was that it was no use mincing matters. Her only resource was in opening\n      a boarding-house, for which the prospects, she judged, were good. Good\n      enough, at any rate, to make her tell him frankly that with two hundred\n      pounds she could make a start. He had torn the envelope open, hastily,\n      on deck, where it was handed to him by the ship-chandler's runner, who\n      had brought his mail at the moment of anchoring. For the second time\n      in his life he was appalled, and remained stock-still at the cabin door\n      with the paper trembling between his fingers. Open a boarding-house! Two\n      hundred pounds for a start! The only resource! And he did not know where\n      to lay his hands on two hundred pence.</p>\n\n      <p>All that night Captain Whalley walked the poop of his anchored ship, as\n      though he had been about to close with the land in thick weather, and\n      uncertain of his position after a run of many gray days without a sight\n      of sun, moon, or stars. The black night twinkled with the guiding lights\n      of seamen and the steady straight lines of lights on shore; and all\n      around the Fair Maid the riding lights of ships cast trembling trails\n      upon the water of the roadstead. Captain Whalley saw not a gleam\n      anywhere till the dawn broke and he found out that his clothing was\n      soaked through with the heavy dew.</p>\n\n      <p>His ship was awake. He stopped short, stroked his wet beard, and\n      descended the poop ladder backwards, with tired feet. At the sight\n      of him the chief officer, lounging about sleepily on the quarterdeck,\n      remained open-mouthed in the middle of a great early-morning yawn.</p>\n\n      <p>\"Good morning to you,\" pronounced Captain Whalley solemnly, passing into\n      the cabin. But he checked himself in the doorway, and without looking\n      back, \"By the bye,\" he said, \"there should be an empty wooden case put\n      away in the lazarette. It has not been broken up--has it?\"</p>\n\n      <p>The mate shut his mouth, and then asked as if dazed, \"What empty case,\n      sir?\"</p>\n\n      <p>\"A big flat packing-case belonging to that painting in my room. Let it\n      be taken up on deck and tell the carpenter to look it over. I may want\n      to use it before long.\"</p>\n\n      <p>The chief officer did not stir a limb till he had heard the door of the\n      captain's state-room slam within the cuddy. Then he beckoned aft the\n      second mate with his forefinger to tell him that there was something \"in\n      the wind.\"</p>\n\n      <p>When the bell rang Captain Whalley's authoritative voice boomed out\n      through a closed door, \"Sit down and don't wait for me.\" And his\n      impressed officers took their places, exchanging looks and whispers\n      across the table. What! No breakfast? And after apparently knocking\n      about all night on deck, too! Clearly, there was something in the wind.\n      In the skylight above their heads, bowed earnestly over the plates,\n      three wire cages rocked and rattled to the restless jumping of the\n      hungry canaries; and they could detect the sounds of their \"old\n      man's\" deliberate movements within his state-room. Captain Whalley was\n      methodically winding up the chronometers, dusting the portrait of\n      his late wife, getting a clean white shirt out of the drawers, making\n      himself ready in his punctilious unhurried manner to go ashore. He could\n      not have swallowed a single mouthful of food that morning. He had made\n      up his mind to sell the Fair Maid.</p>\n    </div>\n\n    <div class=\"pointer\"></div>\n\n    <style>\n      body {\n        cursor: pointer;\n      }\n      .scroll {\n        height: 80vh;\n        width: 80vw;\n        max-height: 600px;\n        position: fixed;\n        top: 5em;\n        left: 10vw;\n\n        overflow-y: scroll;\n        padding: 4em;\n        box-sizing: border-box;\n        line-height: 1.2;\n      }\n      .scroll::-webkit-scrollbar, .scroll::-webkit-scrollbar-track, .scroll::-webkit-scrollbar-thumb {\n        display: none;\n      }\n\n      .pointer {\n        height: 3.6em;\n        width: 77vw;\n        border: 5px solid #CCC;\n        border-radius: 15px;\n        background-color: rgba(0, 0, 0, 0.05);\n        pointer-events: none;\n      }\n      .highlight {\n        background-color: rgba(255, 255, 0, 0.3);\n      }\n      .hover {\n        background-color: rgba(0, 255, 255, 0.2);\n      }\n    </style>\n\n    <script src=\"../../dist/js/tether.js\"></script>\n    <script>\n      var pointer = document.querySelector('.pointer');\n      var scroll = document.querySelector('.scroll');\n\n      // This creates the pointer tether and links it up\n      // with the scroll handle\n      new Tether({\n        element: pointer,\n        target: scroll,\n        attachment: 'middle right',\n        targetAttachment: 'middle left',\n        targetModifier: 'scroll-handle'\n      });\n\n      // Everything after this is for the highlighting effect\n      var paras = document.querySelectorAll('p');\n      for(var i=paras.length; i--;){\n        var sents = paras[i].innerHTML.split('.');\n        for (var j=sents.length; j--;){\n          if (sents[j].trim().length)\n            sents[j] = '<span>' + sents[j] + '.</span>';\n        }\n        paras[i].innerHTML = sents.join('');\n      }\n\n      var spans = document.querySelectorAll('p span');\n\n      function highlight(){\n        if (!spans) return;\n\n        var bar = pointer.getBoundingClientRect();\n\n        for (var i=spans.length; i--;){\n          var coord = spans[i].getBoundingClientRect();\n\n          if (bar.top < coord.top && bar.bottom > coord.top){\n            spans[i].classList.add('hover');\n          } else if (spans[i].classList.contains('hover')) {\n            spans[i].classList.remove('hover');\n          }\n        }\n\n        requestAnimationFrame(highlight);\n      }\n\n      highlight();\n\n      document.body.addEventListener('click', function(){\n        var els = document.querySelectorAll('.hover');\n        for (var i=els.length; i--;)\n          els[i].classList.toggle('highlight');\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/enable-disable/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n        <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n        <link rel=\"stylesheet\" href=\"../common/css/style.css\" />\n    </head>\n    <body>\n        <div class=\"instructions\">Click the green target to enable/disable the tethering.</div>\n\n        <div class=\"element\"></div>\n        <div class=\"container\">\n            <div class=\"pad\"></div>\n            <div class=\"target\"></div>\n            <div class=\"pad\"></div>\n        </div>\n\n        <script src=\"../../dist/js/tether.js\"></script>\n        <script>\n            var tether = new Tether({\n                element: '.element',\n                target: '.target',\n                attachment: 'top left',\n                targetAttachment: 'top right'\n            });\n\n            document.querySelector('.target').addEventListener('click', function(){\n                if (tether.enabled)\n                    tether.disable();\n                else\n                    tether.enable();\n            });\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "examples/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <title>Examples</title>\n</head>\n<body>\n  <h3 id=\"examples\">Examples</h3>\n  <p>It's our goal to create a wide variety of example of how Tether\n    can be used. Here's what we have so far, please send a PR with\n    any examples you might create.</p>\n  <h4 id=\"beginner\">Beginner</h4>\n  <ul>\n    <li><a href=\"./simple\">simple</a>: A simple example to get you started</li>\n    <li><a href=\"./out-of-bounds\">out-of-bounds</a>: How to hide the element when it would\n      otherwise be offscreen</li>\n    <li><a href=\"./pin\">pin</a>: How to pin the element so it never goes offscreen</li>\n    <li><a href=\"./enable-disable\">enable-disable</a>: How to enable and disable the Tether\n      in JavaScript</li>\n  </ul>\n  <h4 id=\"advanced\">Advanced</h4>\n  <ul>\n    <li><a href=\"./content-visible\">content-visible</a>: Demonstrates using the <code>'visible'</code>\n      <code>targetModifier</code> to align an element with the visible portion of another.</li>\n    <li><a href=\"./dolls\">dolls</a>: A performance test to show several dozen elements,\n      each tethered to the previous. Try dragging the top left tether.</li>\n    <li><a href=\"./element-scroll\">element-scroll</a>: Demonstrates using the <code>'scroll-handle'</code>\n      <code>targetModifier</code> to align an element with the scrollbar of an element.</li>\n    <li><a href=\"./scroll\">scroll</a>: Demonstrates using the <code>'scroll-handle'</code>\n      <code>targetModifier</code>\n      to align an element with the body's scroll handle.</li>\n    <li><a href=\"./viewport\">viewport</a>: Demonstrates aligning an element with the\n      viewport by using the <code>'visible'</code> <code>targetModifier</code> when tethered to the body.</li>\n  </ul>\n</body>\n</html>"
  },
  {
    "path": "examples/out-of-bounds/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n        <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n        <link rel=\"stylesheet\" href=\"../common/css/style.css\" />\n        <style>\n            .tether-element.tether-out-of-bounds {\n                display: none;\n            }\n        </style>\n    </head>\n    <body>\n        <div class=\"instructions\">Resize the screen to see the tethered element disappear when it can't fit.</div>\n\n        <div class=\"element\"></div>\n        <div class=\"target\"></div>\n\n        <script src=\"../../dist/js/tether.js\"></script>\n        <script>\n            var tether = new Tether({\n                element: '.element',\n                target: '.target',\n                attachment: 'top left',\n                targetAttachment: 'top right',\n                constraints: [{\n                    to: 'window',\n                    attachment: 'together'\n                }]\n            });\n            tether.on('update', function(event) {\n                console.log(event);\n            });\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "examples/pin/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n        <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n        <link rel=\"stylesheet\" href=\"../common/css/style.css\" />\n    </head>\n    <body>\n        <div class=\"instructions\">Resize the screen to see the tethered element stick to the edges of the screen when it's resized.</div>\n\n        <div class=\"element\"></div>\n        <div class=\"target\"></div>\n\n        <script src=\"../../dist/js/tether.js\"></script>\n        <script>\n            new Tether({\n                element: '.element',\n                target: '.target',\n                attachment: 'top left',\n                targetAttachment: 'top right',\n                constraints: [{\n                    to: 'window',\n                    pin: true\n                }]\n            });\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "examples/resources/css/base.css",
    "content": "body {\n    font-family: \"Helvetica Neue\", sans-serif;\n    color: #444;\n    margin: 0px;\n}\n\na {\n    cursor: pointer;\n    color: blue;\n}"
  },
  {
    "path": "examples/scroll/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n        <link rel=\"stylesheet\" href=\"../common/css/style.css\" />\n    </head>\n    <body>\n\n    <div class=\"instructions\">Scroll the page</div>\n\n    <h2>THE END OF THE TETHER</h2>\n\n    <p>By Joseph Conrad</p>\n\n    <h3>Chapter I</h3>\n\n    <p>For a long time after the course of the steamer <em>Sofala</em> had been\n    altered for the land, the low swampy coast had retained its appearance\n    of a mere smudge of darkness beyond a belt of glitter. The sunrays\n    seemed to fall violently upon the calm sea--seemed to shatter themselves\n    upon an adamantine surface into sparkling dust, into a dazzling vapor\n    of light that blinded the eye and wearied the brain with its unsteady\n    brightness.</p>\n\n    <p>Captain Whalley did not look at it. When his Serang, approaching the\n    roomy cane arm-chair which he filled capably, had informed him in a low\n    voice that the course was to be altered, he had risen at once and had\n    remained on his feet, face forward, while the head of his ship swung\n    through a quarter of a circle. He had not uttered a single word, not\n    even the word to steady the helm. It was the Serang, an elderly, alert,\n    little Malay, with a very dark skin, who murmured the order to the\n    helmsman. And then slowly Captain Whalley sat down again in the\n    arm-chair on the bridge and fixed his eyes on the deck between his feet.</p>\n\n    <p>He could not hope to see anything new upon this lane of the sea. He had\n    been on these coasts for the last three years. From Low Cape to Malantan\n    the distance was fifty miles, six hours' steaming for the old ship with\n    the tide, or seven against. Then you steered straight for the land, and\n    by-and-by three palms would appear on the sky, tall and slim, and with\n    their disheveled heads in a bunch, as if in confidential criticism of\n    the dark mangroves. The Sofala would be headed towards the somber\n    strip of the coast, which at a given moment, as the ship closed with\n    it obliquely, would show several clean shining fractures--the brimful\n    estuary of a river. Then on through a brown liquid, three parts water\n    and one part black earth, on and on between the low shores, three parts\n    black earth and one part brackish water, the Sofala would plow her way\n    up-stream, as she had done once every month for these seven years or\n    more, long before he was aware of her existence, long before he had ever\n    thought of having anything to do with her and her invariable voyages.\n    The old ship ought to have known the road better than her men, who had\n    not been kept so long at it without a change; better than the faithful\n    Serang, whom he had brought over from his last ship to keep the\n    captain's watch; better than he himself, who had been her captain for\n    the last three years only. She could always be depended upon to make her\n    courses. Her compasses were never out. She was no trouble at all to\n    take about, as if her great age had given her knowledge, wisdom, and\n    steadiness. She made her landfalls to a degree of the bearing, and\n    almost to a minute of her allowed time. At any moment, as he sat on\n    the bridge without looking up, or lay sleepless in his bed, simply by\n    reckoning the days and the hours he could tell where he was--the precise\n    spot of the beat. He knew it well too, this monotonous huckster's\n    round, up and down the Straits; he knew its order and its sights and its\n    people. Malacca to begin with, in at daylight and out at dusk, to cross\n    over with a rigid phosphorescent wake this highway of the Far East.\n    Darkness and gleams on the water, clear stars on a black sky, perhaps\n    the lights of a home steamer keeping her unswerving course in the\n    middle, or maybe the elusive shadow of a native craft with her mat sails\n    flitting by silently--and the low land on the other side in sight\n    at daylight. At noon the three palms of the next place of call, up a\n    sluggish river. The only white man residing there was a retired young\n    sailor, with whom he had become friendly in the course of many voyages.\n    Sixty miles farther on there was another place of call, a deep bay with\n    only a couple of houses on the beach. And so on, in and out, picking\n    up coastwise cargo here and there, and finishing with a hundred miles'\n    steady steaming through the maze of an archipelago of small islands up\n    to a large native town at the end of the beat. There was a three days'\n    rest for the old ship before he started her again in inverse order,\n    seeing the same shores from another bearing, hearing the same voices\n    in the same places, back again to the Sofala's port of registry on\n    the great highway to the East, where he would take up a berth nearly\n    opposite the big stone pile of the harbor office till it was time to\n    start again on the old round of 1600 miles and thirty days. Not a very\n    enterprising life, this, for Captain Whalley, Henry Whalley, otherwise\n    Dare-devil Harry--Whalley of the Condor, a famous clipper in her day.\n    No. Not a very enterprising life for a man who had served famous firms,\n    who had sailed famous ships (more than one or two of them his own); who\n    had made famous passages, had been the pioneer of new routes and new\n    trades; who had steered across the unsurveyed tracts of the South Seas,\n    and had seen the sun rise on uncharted islands. Fifty years at sea, and\n    forty out in the East (\"a pretty thorough apprenticeship,\" he used\n    to remark smilingly), had made him honorably known to a generation of\n    shipowners and merchants in all the ports from Bombay clear over to\n    where the East merges into the West upon the coast of the two Americas.\n    His fame remained writ, not very large but plain enough, on the\n    Admiralty charts. Was there not somewhere between Australia and China a\n    Whalley Island and a Condor Reef? On that dangerous coral formation the\n    celebrated clipper had hung stranded for three days, her captain and\n    crew throwing her cargo overboard with one hand and with the other, as\n    it were, keeping off her a flotilla of savage war-canoes. At that time\n    neither the island nor the reef had any official existence. Later the\n    officers of her Majesty's steam vessel Fusilier, dispatched to make a\n    survey of the route, recognized in the adoption of these two names the\n    enterprise of the man and the solidity of the ship. Besides, as anyone\n    who cares may see, the \"General Directory,\" vol. ii. p. 410, begins the\n    description of the \"Malotu or Whalley Passage\" with the words: \"This\n    advantageous route, first discovered in 1850 by Captain Whalley in the\n    ship Condor,\" &amp;c., and ends by recommending it warmly to sailing vessels\n    leaving the China ports for the south in the months from December to\n    April inclusive.</p>\n\n    <p>This was the clearest gain he had out of life. Nothing could rob him\n    of this kind of fame. The piercing of the Isthmus of Suez, like the\n    breaking of a dam, had let in upon the East a flood of new ships, new\n    men, new methods of trade. It had changed the face of the Eastern seas\n    and the very spirit of their life; so that his early experiences meant\n    nothing whatever to the new generation of seamen.</p>\n\n    <p>In those bygone days he had handled many thousands of pounds of his\n    employers' money and of his own; he had attended faithfully, as by law\n    a shipmaster is expected to do, to the conflicting interests of owners,\n    charterers, and underwriters. He had never lost a ship or consented to\n    a shady transaction; and he had lasted well, outlasting in the end the\n    conditions that had gone to the making of his name. He had buried his\n    wife (in the Gulf of Petchili), had married off his daughter to the man\n    of her unlucky choice, and had lost more than an ample competence in the\n    crash of the notorious Travancore and Deccan Banking Corporation, whose\n    downfall had shaken the East like an earthquake. And he was sixty-five\n    years old.</p>\n\n    <h3>Chapter II</h3>\n\n    <p>His age sat lightly enough on him; and of his ruin he was not ashamed.\n    He had not been alone to believe in the stability of the Banking\n    Corporation. Men whose judgment in matters of finance was as expert as\n    his seamanship had commended the prudence of his investments, and had\n    themselves lost much money in the great failure. The only difference\n    between him and them was that he had lost his all. And yet not his all.\n    There had remained to him from his lost fortune a very pretty little\n    bark, Fair Maid, which he had bought to occupy his leisure of a retired\n    sailor--\"to play with,\" as he expressed it himself.</p>\n\n    <p>He had formally declared himself tired of the sea the year preceding his\n    daughter's marriage. But after the young couple had gone to settle in\n    Melbourne he found out that he could not make himself happy on shore. He\n    was too much of a merchant sea-captain for mere yachting to satisfy him.\n    He wanted the illusion of affairs; and his acquisition of the Fair\n    Maid preserved the continuity of his life. He introduced her to his\n    acquaintances in various ports as \"my last command.\" When he grew too\n    old to be trusted with a ship, he would lay her up and go ashore to be\n    buried, leaving directions in his will to have the bark towed out and\n    scuttled decently in deep water on the day of the funeral. His daughter\n    would not grudge him the satisfaction of knowing that no stranger would\n    handle his last command after him. With the fortune he was able to leave\n    her, the value of a 500-ton bark was neither here nor there. All this\n    would be said with a jocular twinkle in his eye: the vigorous old man\n    had too much vitality for the sentimentalism of regret; and a little\n    wistfully withal, because he was at home in life, taking a genuine\n    pleasure in its feelings and its possessions; in the dignity of his\n    reputation and his wealth, in his love for his daughter, and in his\n    satisfaction with the ship--the plaything of his lonely leisure.</p>\n\n    <p>He had the cabin arranged in accordance with his simple ideal of comfort\n    at sea. A big bookcase (he was a great reader) occupied one side of his\n    stateroom; the portrait of his late wife, a flat bituminous oil-painting\n    representing the profile and one long black ringlet of a young woman,\n    faced his bed-place. Three chronometers ticked him to sleep and greeted\n    him on waking with the tiny competition of their beats. He rose at five\n    every day. The officer of the morning watch, drinking his early cup\n    of coffee aft by the wheel, would hear through the wide orifice of the\n    copper ventilators all the splashings, blowings, and splutterings of\n    his captain's toilet. These noises would be followed by a sustained\n    deep murmur of the Lord's Prayer recited in a loud earnest voice. Five\n    minutes afterwards the head and shoulders of Captain Whalley emerged\n    out of the companion-hatchway. Invariably he paused for a while on the\n    stairs, looking all round at the horizon; upwards at the trim of the\n    sails; inhaling deep draughts of the fresh air. Only then he would step\n    out on the poop, acknowledging the hand raised to the peak of the cap\n    with a majestic and benign \"Good morning to you.\" He walked the deck\n    till eight scrupulously. Sometimes, not above twice a year, he had to\n    use a thick cudgel-like stick on account of a stiffness in the hip--a\n    slight touch of rheumatism, he supposed. Otherwise he knew nothing of\n    the ills of the flesh. At the ringing of the breakfast bell he went\n    below to feed his canaries, wind up the chronometers, and take the\n    head of the table. From there he had before his eyes the big carbon\n    photographs of his daughter, her husband, and two fat-legged babies\n    --his grandchildren--set in black frames into the maplewood bulkheads\n    of the cuddy. After breakfast he dusted the glass over these portraits\n    himself with a cloth, and brushed the oil painting of his wife with a\n    plumate kept suspended from a small brass hook by the side of the heavy\n    gold frame. Then with the door of his stateroom shut, he would sit down\n    on the couch under the portrait to read a chapter out of a thick pocket\n    Bible--her Bible. But on some days he only sat there for half an hour\n    with his finger between the leaves and the closed book resting on his\n    knees. Perhaps he had remembered suddenly how fond of boat-sailing she\n    used to be.</p>\n\n    <p>She had been a real shipmate and a true woman too. It was like an\n    article of faith with him that there never had been, and never could be,\n    a brighter, cheerier home anywhere afloat or ashore than his home under\n    the poop-deck of the Condor, with the big main cabin all white and gold,\n    garlanded as if for a perpetual festival with an unfading wreath. She\n    had decorated the center of every panel with a cluster of home flowers.\n    It took her a twelvemonth to go round the cuddy with this labor of love.\n    To him it had remained a marvel of painting, the highest achievement of\n    taste and skill; and as to old Swinburne, his mate, every time he\n    came down to his meals he stood transfixed with admiration before the\n    progress of the work. You could almost smell these roses, he declared,\n    sniffing the faint flavor of turpentine which at that time pervaded the\n    saloon, and (as he confessed afterwards) made him somewhat less hearty\n    than usual in tackling his food. But there was nothing of the sort to\n    interfere with his enjoyment of her singing. \"Mrs. Whalley is a regular\n    out-and-out nightingale, sir,\" he would pronounce with a judicial air\n    after listening profoundly over the skylight to the very end of the\n    piece. In fine weather, in the second dog-watch, the two men could hear\n    her trills and roulades going on to the accompaniment of the piano in\n    the cabin. On the very day they got engaged he had written to London\n    for the instrument; but they had been married for over a year before it\n    reached them, coming out round the Cape. The big case made part of the\n    first direct general cargo landed in Hong-kong harbor--an event that to\n    the men who walked the busy quays of to-day seemed as hazily remote as\n    the dark ages of history. But Captain Whalley could in a half hour of\n    solitude live again all his life, with its romance, its idyl, and its\n    sorrow. He had to close her eyes himself. She went away from under the\n    ensign like a sailor's wife, a sailor herself at heart. He had read\n    the service over her, out of her own prayer-book, without a break in his\n    voice. When he raised his eyes he could see old Swinburne facing him\n    with his cap pressed to his breast, and his rugged, weather-beaten,\n    impassive face streaming with drops of water like a lump of chipped red\n    granite in a shower. It was all very well for that old sea-dog to cry.\n    He had to read on to the end; but after the splash he did not remember\n    much of what happened for the next few days. An elderly sailor of the\n    crew, deft at needlework, put together a mourning frock for the child\n    out of one of her black skirts.</p>\n\n    <p>He was not likely to forget; but you cannot dam up life like a sluggish\n    stream. It will break out and flow over a man's troubles, it will close\n    upon a sorrow like the sea upon a dead body, no matter how much love has\n    gone to the bottom. And the world is not bad. People had been very\n    kind to him; especially Mrs. Gardner, the wife of the senior partner\n    in Gardner, Patteson, &amp; Co., the owners of the Condor. It was she who\n    volunteered to look after the little one, and in due course took her to\n    England (something of a journey in those days, even by the overland\n    mail route) with her own girls to finish her education. It was ten years\n    before he saw her again.</p>\n\n    <p>As a little child she had never been frightened of bad weather; she\n    would beg to be taken up on deck in the bosom of his oilskin coat to\n    watch the big seas hurling themselves upon the Condor. The swirl and\n    crash of the waves seemed to fill her small soul with a breathless\n    delight. \"A good boy spoiled,\" he used to say of her in joke. He had\n    named her Ivy because of the sound of the word, and obscurely fascinated\n    by a vague association of ideas. She had twined herself tightly round\n    his heart, and he intended her to cling close to her father as to a\n    tower of strength; forgetting, while she was little, that in the nature\n    of things she would probably elect to cling to someone else. But\n    he loved life well enough for even that event to give him a certain\n    satisfaction, apart from his more intimate feeling of loss.</p>\n\n    <p>After he had purchased the Fair Maid to occupy his loneliness, he\n    hastened to accept a rather unprofitable freight to Australia simply for\n    the opportunity of seeing his daughter in her own home. What made him\n    dissatisfied there was not to see that she clung now to somebody else,\n    but that the prop she had selected seemed on closer examination \"a\n    rather poor stick\"--even in the matter of health. He disliked his\n    son-in-law's studied civility perhaps more than his method of\n    handling the sum of money he had given Ivy at her marriage. But of his\n    apprehensions he said nothing. Only on the day of his departure, with\n    the hall-door open already, holding her hands and looking steadily into\n    her eyes, he had said, \"You know, my dear, all I have is for you and the\n    chicks. Mind you write to me openly.\" She had answered him by an almost\n    imperceptible movement of her head. She resembled her mother in\n    the color of her eyes, and in character--and also in this, that she\n    understood him without many words.</p>\n\n    <p>Sure enough she had to write; and some of these letters made Captain\n    Whalley lift his white eye-brows. For the rest he considered he was\n    reaping the true reward of his life by being thus able to produce on\n    demand whatever was needed. He had not enjoyed himself so much in a\n    way since his wife had died. Characteristically enough his son-in-law's\n    punctuality in failure caused him at a distance to feel a sort of\n    kindness towards the man. The fellow was so perpetually being jammed on\n    a lee shore that to charge it all to his reckless navigation would be\n    manifestly unfair. No, no! He knew well what that meant. It was bad\n    luck. His own had been simply marvelous, but he had seen in his life too\n    many good men--seamen and others--go under with the sheer weight of bad\n    luck not to recognize the fatal signs. For all that, he was cogitating\n    on the best way of tying up very strictly every penny he had to leave,\n    when, with a preliminary rumble of rumors (whose first sound reached\n    him in Shanghai as it happened), the shock of the big failure came;\n    and, after passing through the phases of stupor, of incredulity, of\n    indignation, he had to accept the fact that he had nothing to speak of\n    to leave.</p>\n\n    <p>Upon that, as if he had only waited for this catastrophe, the unlucky\n    man, away there in Melbourne, gave up his unprofitable game, and sat\n    down--in an invalid's bath-chair at that too. \"He will never walk\n    again,\" wrote the wife. For the first time in his life Captain Whalley\n    was a bit staggered.</p>\n\n    <p>The Fair Maid had to go to work in bitter earnest now. It was no longer\n    a matter of preserving alive the memory of Dare-devil Harry Whalley in\n    the Eastern Seas, or of keeping an old man in pocket-money and clothes,\n    with, perhaps, a bill for a few hundred first-class cigars thrown in at\n    the end of the year. He would have to buckle-to, and keep her going hard\n    on a scant allowance of gilt for the ginger-bread scrolls at her stem\n    and stern.</p>\n\n    <p>This necessity opened his eyes to the fundamental changes of the world.\n    Of his past only the familiar names remained, here and there, but\n    the things and the men, as he had known them, were gone. The name of\n    Gardner, Patteson, &amp; Co. was still displayed on the walls of warehouses\n    by the waterside, on the brass plates and window-panes in the business\n    quarters of more than one Eastern port, but there was no longer a\n    Gardner or a Patteson in the firm. There was no longer for Captain\n    Whalley an arm-chair and a welcome in the private office, with a bit of\n    business ready to be put in the way of an old friend, for the sake of\n    bygone services. The husbands of the Gardner girls sat behind the desks\n    in that room where, long after he had left the employ, he had kept his\n    right of entrance in the old man's time. Their ships now had yellow\n    funnels with black tops, and a time-table of appointed routes like a\n    confounded service of tramways. The winds of December and June were all\n    one to them; their captains (excellent young men he doubted not) were,\n    to be sure, familiar with Whalley Island, because of late years the\n    Government had established a white fixed light on the north end (with\n    a red danger sector over the Condor Reef), but most of them would have\n    been extremely surprised to hear that a flesh-and-blood Whalley still\n    existed--an old man going about the world trying to pick up a cargo here\n    and there for his little bark.</p>\n\n    <p>And everywhere it was the same. Departed the men who would have nodded\n    appreciatively at the mention of his name, and would have thought\n    themselves bound in honor to do something for Dare-devil Harry Whalley.\n    Departed the opportunities which he would have known how to seize; and\n    gone with them the white-winged flock of clippers that lived in the\n    boisterous uncertain life of the winds, skimming big fortunes out of\n    the foam of the sea. In a world that pared down the profits to an\n    irreducible minimum, in a world that was able to count its disengaged\n    tonnage twice over every day, and in which lean charters were snapped up\n    by cable three months in advance, there were no chances of fortune for\n    an individual wandering haphazard with a little bark--hardly indeed any\n    room to exist.</p>\n\n    <p>He found it more difficult from year to year. He suffered greatly from\n    the smallness of remittances he was able to send his daughter. Meantime\n    he had given up good cigars, and even in the matter of inferior cheroots\n    limited himself to six a day. He never told her of his difficulties, and\n    she never enlarged upon her struggle to live. Their confidence in each\n    other needed no explanations, and their perfect understanding endured\n    without protestations of gratitude or regret. He would have been shocked\n    if she had taken it into her head to thank him in so many words, but\n    he found it perfectly natural that she should tell him she needed two\n    hundred pounds.</p>\n\n    <p>He had come in with the Fair Maid in ballast to look for a freight in\n    the Sofala's port of registry, and her letter met him there. Its tenor\n    was that it was no use mincing matters. Her only resource was in opening\n    a boarding-house, for which the prospects, she judged, were good. Good\n    enough, at any rate, to make her tell him frankly that with two hundred\n    pounds she could make a start. He had torn the envelope open, hastily,\n    on deck, where it was handed to him by the ship-chandler's runner, who\n    had brought his mail at the moment of anchoring. For the second time\n    in his life he was appalled, and remained stock-still at the cabin door\n    with the paper trembling between his fingers. Open a boarding-house! Two\n    hundred pounds for a start! The only resource! And he did not know where\n    to lay his hands on two hundred pence.</p>\n\n    <p>All that night Captain Whalley walked the poop of his anchored ship, as\n    though he had been about to close with the land in thick weather, and\n    uncertain of his position after a run of many gray days without a sight\n    of sun, moon, or stars. The black night twinkled with the guiding lights\n    of seamen and the steady straight lines of lights on shore; and all\n    around the Fair Maid the riding lights of ships cast trembling trails\n    upon the water of the roadstead. Captain Whalley saw not a gleam\n    anywhere till the dawn broke and he found out that his clothing was\n    soaked through with the heavy dew.</p>\n\n    <p>His ship was awake. He stopped short, stroked his wet beard, and\n    descended the poop ladder backwards, with tired feet. At the sight\n    of him the chief officer, lounging about sleepily on the quarterdeck,\n    remained open-mouthed in the middle of a great early-morning yawn.</p>\n\n    <p>\"Good morning to you,\" pronounced Captain Whalley solemnly, passing into\n    the cabin. But he checked himself in the doorway, and without looking\n    back, \"By the bye,\" he said, \"there should be an empty wooden case put\n    away in the lazarette. It has not been broken up--has it?\"</p>\n\n    <p>The mate shut his mouth, and then asked as if dazed, \"What empty case,\n    sir?\"</p>\n\n    <p>\"A big flat packing-case belonging to that painting in my room. Let it\n    be taken up on deck and tell the carpenter to look it over. I may want\n    to use it before long.\"</p>\n\n    <p>The chief officer did not stir a limb till he had heard the door of the\n    captain's state-room slam within the cuddy. Then he beckoned aft the\n    second mate with his forefinger to tell him that there was something \"in\n    the wind.\"</p>\n\n    <p>When the bell rang Captain Whalley's authoritative voice boomed out\n    through a closed door, \"Sit down and don't wait for me.\" And his\n    impressed officers took their places, exchanging looks and whispers\n    across the table. What! No breakfast? And after apparently knocking\n    about all night on deck, too! Clearly, there was something in the wind.\n    In the skylight above their heads, bowed earnestly over the plates,\n    three wire cages rocked and rattled to the restless jumping of the\n    hungry canaries; and they could detect the sounds of their \"old\n    man's\" deliberate movements within his state-room. Captain Whalley was\n    methodically winding up the chronometers, dusting the portrait of\n    his late wife, getting a clean white shirt out of the drawers, making\n    himself ready in his punctilious unhurried manner to go ashore. He could\n    not have swallowed a single mouthful of food that morning. He had made\n    up his mind to sell the Fair Maid.</p>\n\n    <h3>Chapter III</h3>\n\n    <p>Just at that time the Japanese were casting far and wide for ships\n    of European build, and he had no difficulty in finding a purchaser, a\n    speculator who drove a hard bargain, but paid cash down for the Fair\n    Maid, with a view to a profitable resale. Thus it came about that\n    Captain Whalley found himself on a certain afternoon descending the\n    steps of one of the most important post-offices of the East with a slip\n    of bluish paper in his hand. This was the receipt of a registered letter\n    enclosing a draft for two hundred pounds, and addressed to Melbourne.\n    Captain Whalley pushed the paper into his waistcoat-pocket, took his\n    stick from under his arm, and walked down the street.</p>\n\n    <p>It was a recently opened and untidy thoroughfare with rudimentary\n    side-walks and a soft layer of dust cushioning the whole width of\n    the road. One end touched the slummy street of Chinese shops near the\n    harbor, the other drove straight on, without houses, for a couple of\n    miles, through patches of jungle-like vegetation, to the yard gates\n    of the new Consolidated Docks Company. The crude frontages of the new\n    Government buildings alternated with the blank fencing of vacant plots,\n    and the view of the sky seemed to give an added spaciousness to the\n    broad vista. It was empty and shunned by natives after business\n    hours, as though they had expected to see one of the tigers from the\n    neighborhood of the New Waterworks on the hill coming at a loping canter\n    down the middle to get a Chinese shopkeeper for supper. Captain Whalley\n    was not dwarfed by the solitude of the grandly planned street. He\n    had too fine a presence for that. He was only a lonely figure walking\n    purposefully, with a great white beard like a pilgrim, and with a thick\n    stick that resembled a weapon. On one side the new Courts of Justice had\n    a low and unadorned portico of squat columns half concealed by a few old\n    trees left in the approach. On the other the pavilion wings of the\n    new Colonial Treasury came out to the line of the street. But Captain\n    Whalley, who had now no ship and no home, remembered in passing that\n    on that very site when he first came out from England there had stood a\n    fishing village, a few mat huts erected on piles between a muddy tidal\n    creek and a miry pathway that went writhing into a tangled wilderness\n    without any docks or waterworks.</p>\n\n    <p>No ship--no home. And his poor Ivy away there had no home either. A\n    boarding-house is no sort of home though it may get you a living. His\n    feelings were horribly rasped by the idea of the boarding-house. In his\n    rank of life he had that truly aristocratic temperament characterized by\n    a scorn of vulgar gentility and by prejudiced views as to the derogatory\n    nature of certain occupations. For his own part he had always preferred\n    sailing merchant ships (which is a straightforward occupation) to buying\n    and selling merchandise, of which the essence is to get the better of\n    somebody in a bargain--an undignified trial of wits at best. His father\n    had been Colonel Whalley (retired) of the H. E. I. Company's service,\n    with very slender means besides his pension, but with distinguished\n    connections. He could remember as a boy how frequently waiters at the\n    inns, country tradesmen and small people of that sort, used to \"My lord\"\n    the old warrior on the strength of his appearance.</p>\n\n    <p>Captain Whalley himself (he would have entered the Navy if his father\n    had not died before he was fourteen) had something of a grand air which\n    would have suited an old and glorious admiral; but he became lost like\n    a straw in the eddy of a brook amongst the swarm of brown and yellow\n    humanity filling a thoroughfare, that by contrast with the vast and\n    empty avenue he had left seemed as narrow as a lane and absolutely\n    riotous with life. The walls of the houses were blue; the shops of the\n    Chinamen yawned like cavernous lairs; heaps of nondescript merchandise\n    overflowed the gloom of the long range of arcades, and the fiery\n    serenity of sunset took the middle of the street from end to end with a\n    glow like the reflection of a fire. It fell on the bright colors and the\n    dark faces of the bare-footed crowd, on the pallid yellow backs of the\n    half-naked jostling coolies, on the accouterments of a tall Sikh trooper\n    with a parted beard and fierce mustaches on sentry before the gate of\n    the police compound. Looming very big above the heads in a red haze of\n    dust, the tightly packed car of the cable tramway navigated cautiously\n    up the human stream, with the incessant blare of its horn, in the manner\n    of a steamer groping in a fog.</p>\n\n    <p>Captain Whalley emerged like a diver on the other side, and in the\n    desert shade between the walls of closed warehouses removed his hat to\n    cool his brow. A certain disrepute attached to the calling of a\n    landlady of a boarding-house. These women were said to be rapacious,\n    unscrupulous, untruthful; and though he contemned no class of his\n    fellow-creatures--God forbid!--these were suspicions to which it was\n    unseemly that a Whalley should lay herself open. He had not expostulated\n    with her, however. He was confident she shared his feelings; he was\n    sorry for her; he trusted her judgment; he considered it a merciful\n    dispensation that he could help her once more,--but in his aristocratic\n    heart of hearts he would have found it more easy to reconcile himself to\n    the idea of her turning seamstress. Vaguely he remembered reading years\n    ago a touching piece called the \"Song of the Shirt.\" It was all very\n    well making songs about poor women. The granddaughter of Colonel\n    Whalley, the landlady of a boarding-house! Pooh! He replaced his hat,\n    dived into two pockets, and stopping a moment to apply a flaring match\n    to the end of a cheap cheroot, blew an embittered cloud of smoke at a\n    world that could hold such surprises.</p>\n\n    <p>Of one thing he was certain--that she was the own child of a clever\n    mother. Now he had got over the wrench of parting with his ship, he\n    perceived clearly that such a step had been unavoidable. Perhaps he had\n    been growing aware of it all along with an unconfessed knowledge. But\n    she, far away there, must have had an intuitive perception of it, with\n    the pluck to face that truth and the courage to speak out--all the\n    qualities which had made her mother a woman of such excellent counsel.</p>\n\n    <p>It would have had to come to that in the end! It was fortunate she had\n    forced his hand. In another year or two it would have been an utterly\n    barren sale. To keep the ship going he had been involving himself deeper\n    every year. He was defenseless before the insidious work of adversity,\n    to whose more open assaults he could present a firm front; like a\n    cliff that stands unmoved the open battering of the sea, with a lofty\n    ignorance of the treacherous backwash undermining its base. As it was,\n    every liability satisfied, her request answered, and owing no man a\n    penny, there remained to him from the proceeds a sum of five hundred\n    pounds put away safely. In addition he had upon his person some forty\n    odd dollars--enough to pay his hotel bill, providing he did not linger\n    too long in the modest bedroom where he had taken refuge.</p>\n\n    <p>Scantily furnished, and with a waxed floor, it opened into one of\n    the side-verandas. The straggling building of bricks, as airy as a\n    bird-cage, resounded with the incessant flapping of rattan screens\n    worried by the wind between the white-washed square pillars of the\n    sea-front. The rooms were lofty, a ripple of sunshine flowed over the\n    ceilings; and the periodical invasions of tourists from some passenger\n    steamer in the harbor flitted through the wind-swept dusk of the\n    apartments with the tumult of their unfamiliar voices and impermanent\n    presences, like relays of migratory shades condemned to speed headlong\n    round the earth without leaving a trace. The babble of their irruptions\n    ebbed out as suddenly as it had arisen; the draughty corridors and\n    the long chairs of the verandas knew their sight-seeing hurry or\n    their prostrate repose no more; and Captain Whalley, substantial and\n    dignified, left well-nigh alone in the vast hotel by each light-hearted\n    skurry, felt more and more like a stranded tourist with no aim in view,\n    like a forlorn traveler without a home. In the solitude of his room he\n    smoked thoughtfully, gazing at the two sea-chests which held all that he\n    could call his own in this world. A thick roll of charts in a sheath\n    of sailcloth leaned in a corner; the flat packing-case containing the\n    portrait in oils and the three carbon photographs had been pushed under\n    the bed. He was tired of discussing terms, of assisting at surveys, of\n    all the routine of the business. What to the other parties was merely\n    the sale of a ship was to him a momentous event involving a radically\n    new view of existence. He knew that after this ship there would be no\n    other; and the hopes of his youth, the exercise of his abilities, every\n    feeling and achievement of his manhood, had been indissolubly connected\n    with ships. He had served ships; he had owned ships; and even the years\n    of his actual retirement from the sea had been made bearable by the idea\n    that he had only to stretch out his hand full of money to get a ship. He\n    had been at liberty to feel as though he were the owner of all the\n    ships in the world. The selling of this one was weary work; but when\n    she passed from him at last, when he signed the last receipt, it was as\n    though all the ships had gone out of the world together, leaving him on\n    the shore of inaccessible oceans with seven hundred pounds in his hands.</p>\n\n    <p>Striding firmly, without haste, along the quay, Captain Whalley averted\n    his glances from the familiar roadstead. Two generations of seamen born\n    since his first day at sea stood between him and all these ships at the\n    anchorage. His own was sold, and he had been asking himself, What next?</p>\n\n    <p>From the feeling of loneliness, of inward emptiness,--and of loss\n    too, as if his very soul had been taken out of him forcibly,--there had\n    sprung at first a desire to start right off and join his daughter.\n    \"Here are the last pence,\" he would say to her; \"take them, my dear. And\n    here's your old father: you must take him too.\"</p>\n\n    <p>His soul recoiled, as if afraid of what lay hidden at the bottom of\n    this impulse. Give up! Never! When one is thoroughly weary all sorts of\n    nonsense come into one's head. A pretty gift it would have been for a\n    poor woman--this seven hundred pounds with the incumbrance of a hale old\n    fellow more than likely to last for years and years to come. Was he not\n    as fit to die in harness as any of the youngsters in charge of these\n    anchored ships out yonder? He was as solid now as ever he had been. But\n    as to who would give him work to do, that was another matter. Were he,\n    with his appearance and antecedents, to go about looking for a junior's\n    berth, people, he was afraid, would not take him seriously; or else if\n    he succeeded in impressing them, he would maybe obtain their pity, which\n    would be like stripping yourself naked to be kicked. He was not anxious\n    to give himself away for less than nothing. He had no use for anybody's\n    pity. On the other hand, a command--the only thing he could try for with\n    due regard for common decency--was not likely to be lying in wait\n    for him at the corner of the next street. Commands don't go a-begging\n    nowadays. Ever since he had come ashore to carry out the business of\n    the sale he had kept his ears open, but had heard no hint of one being\n    vacant in the port. And even if there had been one, his successful past\n    itself stood in his way. He had been his own employer too long. The only\n    credential he could produce was the testimony of his whole life. What\n    better recommendation could anyone require? But vaguely he felt that\n    the unique document would be looked upon as an archaic curiosity of the\n    Eastern waters, a screed traced in obsolete words--in a half-forgotten\n    language.</p>\n\n    <h3>Chapter IV</h3>\n\n    <p>Revolving these thoughts, he strolled on near the railings of the quay,\n    broad-chested, without a stoop, as though his big shoulders had never\n    felt the burden of the loads that must be carried between the cradle\n    and the grave. No single betraying fold or line of care disfigured the\n    reposeful modeling of his face. It was full and untanned; and the upper\n    part emerged, massively quiet, out of the downward flow of silvery hair,\n    with the striking delicacy of its clear complexion and the powerful\n    width of the forehead. The first cast of his glance fell on you candid\n    and swift, like a boy's; but because of the ragged snowy thatch of the\n    eyebrows the affability of his attention acquired the character of a\n    dark and searching scrutiny. With age he had put on flesh a little, had\n    increased his girth like an old tree presenting no symptoms of decay;\n    and even the opulent, lustrous ripple of white hairs upon his chest\n    seemed an attribute of unquenchable vitality and vigor.</p>\n\n    <p>Once rather proud of his great bodily strength, and even of his personal\n    appearance, conscious of his worth, and firm in his rectitude, there had\n    remained to him, like the heritage of departed prosperity, the tranquil\n    bearing of a man who had proved himself fit in every sort of way for the\n    life of his choice. He strode on squarely under the projecting brim of\n    an ancient Panama hat. It had a low crown, a crease through its whole\n    diameter, a narrow black ribbon. Imperishable and a little discolored,\n    this headgear made it easy to pick him out from afar on thronged wharves\n    and in the busy streets. He had never adopted the comparatively modern\n    fashion of pipeclayed cork helmets. He disliked the form; and he hoped\n    he could manage to keep a cool head to the end of his life without all\n    these contrivances for hygienic ventilation. His hair was cropped close,\n    his linen always of immaculate whiteness; a suit of thin gray flannel,\n    worn threadbare but scrupulously brushed, floated about his burly limbs,\n    adding to his bulk by the looseness of its cut. The years had mellowed\n    the good-humored, imperturbable audacity of his prime into a temper\n    carelessly serene; and the leisurely tapping of his iron-shod stick\n    accompanied his footfalls with a self-confident sound on the flagstones.\n    It was impossible to connect such a fine presence and this unruffled\n    aspect with the belittling troubles of poverty; the man's whole\n    existence appeared to pass before you, facile and large, in the freedom\n    of means as ample as the clothing of his body.</p>\n\n    <p>The irrational dread of having to break into his five hundred pounds for\n    personal expenses in the hotel disturbed the steady poise of his mind.\n    There was no time to lose. The bill was running up. He nourished the\n    hope that this five hundred would perhaps be the means, if everything\n    else failed, of obtaining some work which, keeping his body and soul\n    together (not a matter of great outlay), would enable him to be of use\n    to his daughter. To his mind it was her own money which he employed, as\n    it were, in backing her father and solely for her benefit. Once at work,\n    he would help her with the greater part of his earnings; he was good for\n    many years yet, and this boarding-house business, he argued to himself,\n    whatever the prospects, could not be much of a gold-mine from the first\n    start. But what work? He was ready to lay hold of anything in an honest\n    way so that it came quickly to his hand; because the five hundred pounds\n    must be preserved intact for eventual use. That was the great point.\n    With the entire five hundred one felt a substance at one's back; but\n    it seemed to him that should he let it dwindle to four-fifty or even\n    four-eighty, all the efficiency would be gone out of the money, as though\n    there were some magic power in the round figure. But what sort of work?</p>\n\n    <p>Confronted by that haunting question as by an uneasy ghost, for whom he\n    had no exorcising formula, Captain Whalley stopped short on the apex\n    of a small bridge spanning steeply the bed of a canalized creek with\n    granite shores. Moored between the square blocks a seagoing Malay prau\n    floated half hidden under the arch of masonry, with her spars lowered\n    down, without a sound of life on board, and covered from stem to stern\n    with a ridge of palm-leaf mats. He had left behind him the overheated\n    pavements bordered by the stone frontages that, like the sheer face of\n    cliffs, followed the sweep of the quays; and an unconfined spaciousness\n    of orderly and sylvan aspect opened before him its wide plots of rolled\n    grass, like pieces of green carpet smoothly pegged out, its long ranges\n    of trees lined up in colossal porticos of dark shafts roofed with a\n    vault of branches.</p>\n\n    <p>Some of these avenues ended at the sea. It was a terraced shore; and\n    beyond, upon the level expanse, profound and glistening like the gaze\n    of a dark-blue eye, an oblique band of stippled purple lengthened itself\n    indefinitely through the gap between a couple of verdant twin islets.\n    The masts and spars of a few ships far away, hull down in the outer\n    roads, sprang straight from the water in a fine maze of rosy lines\n    penciled on the clear shadow of the eastern board. Captain Whalley gave\n    them a long glance. The ship, once his own, was anchored out there. It\n    was staggering to think that it was open to him no longer to take a boat\n    at the jetty and get himself pulled off to her when the evening came. To\n    no ship. Perhaps never more. Before the sale was concluded, and till the\n    purchase-money had been paid, he had spent daily some time on board the\n    Fair Maid. The money had been paid this very morning, and now, all at\n    once, there was positively no ship that he could go on board of when he\n    liked; no ship that would need his presence in order to do her work--to\n    live. It seemed an incredible state of affairs, something too bizarre\n    to last. And the sea was full of craft of all sorts. There was that prau\n    lying so still swathed in her shroud of sewn palm-leaves--she too had\n    her indispensable man. They lived through each other, this Malay he had\n    never seen, and this high-sterned thing of no size that seemed to be\n    resting after a long journey. And of all the ships in sight, near and\n    far, each was provided with a man, the man without whom the finest ship\n    is a dead thing, a floating and purposeless log.</p>\n\n    <p>After his one glance at the roadstead he went on, since there was\n    nothing to turn back for, and the time must be got through somehow. The\n    avenues of big trees ran straight over the Esplanade, cutting each other\n    at diverse angles, columnar below and luxuriant above. The interlaced\n    boughs high up there seemed to slumber; not a leaf stirred overhead:\n    and the reedy cast-iron lampposts in the middle of the road, gilt like\n    scepters, diminished in a long perspective, with their globes of white\n    porcelain atop, resembling a barbarous decoration of ostriches' eggs\n    displayed in a row. The flaming sky kindled a tiny crimson spark upon\n    the glistening surface of each glassy shell.</p>\n\n    <p>With his chin sunk a little, his hands behind his back, and the end of\n    his stick marking the gravel with a faint wavering line at his heels,\n    Captain Whalley reflected that if a ship without a man was like a body\n    without a soul, a sailor without a ship was of not much more account\n    in this world than an aimless log adrift upon the sea. The log might be\n    sound enough by itself, tough of fiber, and hard to destroy--but what of\n    that! And a sudden sense of irremediable idleness weighted his feet like\n    a great fatigue.</p>\n\n    <p>A succession of open carriages came bowling along the newly opened\n    sea-road. You could see across the wide grass-plots the discs of\n    vibration made by the spokes. The bright domes of the parasols swayed\n    lightly outwards like full-blown blossoms on the rim of a vase; and\n    the quiet sheet of dark-blue water, crossed by a bar of purple, made a\n    background for the spinning wheels and the high action of the horses,\n    whilst the turbaned heads of the Indian servants elevated above the line\n    of the sea horizon glided rapidly on the paler blue of the sky. In an\n    open space near the little bridge each turn-out trotted smartly in a\n    wide curve away from the sunset; then pulling up sharp, entered the main\n    alley in a long slow-moving file with the great red stillness of the sky\n    at the back. The trunks of mighty trees stood all touched with red on\n    the same side, the air seemed aflame under the high foliage, the\n    very ground under the hoofs of the horses was red. The wheels turned\n    solemnly; one after another the sunshades drooped, folding their colors\n    like gorgeous flowers shutting their petals at the end of the day. In\n    the whole half-mile of human beings no voice uttered a distinct word,\n    only a faint thudding noise went on mingled with slight jingling sounds,\n    and the motionless heads and shoulders of men and women sitting in\n    couples emerged stolidly above the lowered hoods--as if wooden. But one\n    carriage and pair coming late did not join the line.</p>\n\n    <p>It fled along in a noiseless roll; but on entering the avenue one of the\n    dark bays snorted, arching his neck and shying against the steel-tipped\n    pole; a flake of foam fell from the bit upon the point of a satiny\n    shoulder, and the dusky face of the coachman leaned forward at once over\n    the hands taking a fresh grip of the reins. It was a long dark-green\n    landau, having a dignified and buoyant motion between the sharply\n    curved C-springs, and a sort of strictly official majesty in its supreme\n    elegance. It seemed more roomy than is usual, its horses seemed slightly\n    bigger, the appointments a shade more perfect, the servants perched\n    somewhat higher on the box. The dresses of three women--two young\n    and pretty, and one, handsome, large, of mature age--seemed to fill\n    completely the shallow body of the carriage. The fourth face was that\n    of a man, heavy lidded, distinguished and sallow, with a somber, thick,\n    iron-gray imperial and mustaches, which somehow had the air of solid\n    appendages. His Excellency--</p>\n\n    <p>The rapid motion of that one equipage made all the others appear utterly\n    inferior, blighted, and reduced to crawl painfully at a snail's pace.\n    The landau distanced the whole file in a sort of sustained rush; the\n    features of the occupant whirling out of sight left behind an impression\n    of fixed stares and impassive vacancy; and after it had vanished in full\n    flight as it were, notwithstanding the long line of vehicles hugging the\n    curb at a walk, the whole lofty vista of the avenue seemed to lie open\n    and emptied of life in the enlarged impression of an august solitude.</p>\n\n    <p>Captain Whalley had lifted his head to look, and his mind, disturbed in\n    its meditation, turned with wonder (as men's minds will do) to matters\n    of no importance. It struck him that it was to this port, where he had\n    just sold his last ship, that he had come with the very first he had\n    ever owned, and with his head full of a plan for opening a new trade\n    with a distant part of the Archipelago. The then governor had given\n    him no end of encouragement. No Excellency he--this Mr. Denham--this\n    governor with his jacket off; a man who tended night and day, so to\n    speak, the growing prosperity of the settlement with the self-forgetful\n    devotion of a nurse for a child she loves; a lone bachelor who lived as\n    in a camp with the few servants and his three dogs in what was called\n    then the Government Bungalow: a low-roofed structure on the half-cleared\n    slope of a hill, with a new flagstaff in front and a police orderly on\n    the veranda. He remembered toiling up that hill under a heavy sun for\n    his audience; the unfurnished aspect of the cool shaded room; the long\n    table covered at one end with piles of papers, and with two guns, a\n    brass telescope, a small bottle of oil with a feather stuck in the neck\n    at the other--and the flattering attention given to him by the man in\n    power. It was an undertaking full of risk he had come to expound, but a\n    twenty minutes' talk in the Government Bungalow on the hill had made it\n    go smoothly from the start. And as he was retiring Mr. Denham, already\n    seated before the papers, called out after him, \"Next month the Dido\n    starts for a cruise that way, and I shall request her captain officially\n    to give you a look in and see how you get on.\" The Dido was one of the\n    smart frigates on the China station--and five-and-thirty years make a\n    big slice of time. Five-and-thirty years ago an enterprise like his had\n    for the colony enough importance to be looked after by a Queen's ship.\n    A big slice of time. Individuals were of some account then. Men like\n    himself; men, too, like poor Evans, for instance, with his red face,\n    his coal-black whiskers, and his restless eyes, who had set up the first\n    patent slip for repairing small ships, on the edge of the forest, in\n    a lonely bay three miles up the coast. Mr. Denham had encouraged that\n    enterprise too, and yet somehow poor Evans had ended by dying at\n    home deucedly hard up. His son, they said, was squeezing oil out of\n    cocoa-nuts for a living on some God-forsaken islet of the Indian Ocean;\n    but it was from that patent slip in a lonely wooded bay that had sprung\n    the workshops of the Consolidated Docks Company, with its three\n    graving basins carved out of solid rock, its wharves, its jetties,\n    its electric-light plant, its steam-power houses--with its gigantic\n    sheer-legs, fit to lift the heaviest weight ever carried afloat, and\n    whose head could be seen like the top of a queer white monument peeping\n    over bushy points of land and sandy promontories, as you approached the\n    New Harbor from the west.</p>\n\n    <p>There had been a time when men counted: there were not so many carriages\n    in the colony then, though Mr. Denham, he fancied, had a buggy. And\n    Captain Whalley seemed to be swept out of the great avenue by the swirl\n    of a mental backwash. He remembered muddy shores, a harbor without\n    quays, the one solitary wooden pier (but that was a public work) jutting\n    out crookedly, the first coal-sheds erected on Monkey Point, that caught\n    fire mysteriously and smoldered for days, so that amazed ships came\n    into a roadstead full of sulphurous smoke, and the sun hung blood-red\n    at midday. He remembered the things, the faces, and something more\n    besides--like the faint flavor of a cup quaffed to the bottom, like a\n    subtle sparkle of the air that was not to be found in the atmosphere of\n    to-day.</p>\n\n    <p>In this evocation, swift and full of detail like a flash of magnesium\n    light into the niches of a dark memorial hall, Captain Whalley\n    contemplated things once important, the efforts of small men, the growth\n    of a great place, but now robbed of all consequence by the greatness\n    of accomplished facts, by hopes greater still; and they gave him for a\n    moment such an almost physical grip upon time, such a comprehension of\n    our unchangeable feelings, that he stopped short, struck the ground with\n    his stick, and ejaculated mentally, \"What the devil am I doing here!\" He\n    seemed lost in a sort of surprise; but he heard his name called out in\n    wheezy tones once, twice--and turned on his heels slowly.</p>\n\n    <p>He beheld then, waddling towards him autocratically, a man of an\n    old-fashioned and gouty aspect, with hair as white as his own, but with\n    shaved, florid cheeks, wearing a necktie--almost a neckcloth--whose\n    stiff ends projected far beyond his chin; with round legs, round arms,\n    a round body, a round face--generally producing the effect of his short\n    figure having been distended by means of an air-pump as much as the\n    seams of his clothing would stand. This was the Master-Attendant of the\n    port. A master-attendant is a superior sort of harbor-master; a person,\n    out in the East, of some consequence in his sphere; a Government\n    official, a magistrate for the waters of the port, and possessed of vast\n    but ill-defined disciplinary authority over seamen of all classes.\n    This particular Master-Attendant was reported to consider it miserably\n    inadequate, on the ground that it did not include the power of life\n    and death. This was a jocular exaggeration. Captain Eliott was fairly\n    satisfied with his position, and nursed no inconsiderable sense of such\n    power as he had. His conceited and tyrannical disposition did not allow\n    him to let it dwindle in his hands for want of use. The uproarious,\n    choleric frankness of his comments on people's character and conduct\n    caused him to be feared at bottom; though in conversation many pretended\n    not to mind him in the least, others would only smile sourly at the\n    mention of his name, and there were even some who dared to pronounce him\n    \"a meddlesome old ruffian.\" But for almost all of them one of Captain\n    Eliott's outbreaks was nearly as distasteful to face as a chance of\n    annihilation.</p>\n\n      <style>\n        body {\n          padding: 15px;\n        }\n\n        .pointer {\n          padding: 15px;\n          background-color: rgba(0, 0, 0, 0.4);\n          color: white;\n          border-radius: 10px;\n          pointer-events: none;\n          opacity: 0;\n\n          transition: opacity 300ms;\n          -webkit-transition: opacity 300ms;\n        }\n\n        .pointer.show {\n          opacity: 1;\n        }\n      </style>\n\n\n      <div class=\"pointer\"></div>\n\n      <script src=\"../../dist/js/tether.js\"></script>\n      <script>\n        new Tether({\n          element: '.pointer',\n          attachment: 'middle right',\n          targetAttachment: 'middle left',\n          targetModifier: 'scroll-handle',\n          target: document.body\n        });\n\n        var headers = document.querySelectorAll('h1,h2,h3,h4,h5,h6');\n        var hideTimeout = null;\n        var pointer = document.querySelector('.pointer')\n\n        var getSection = function(){\n          var closest, closestTop;\n          for (var i=0; i < headers.length; i++){\n            var rect = headers[i].getBoundingClientRect();\n\n            if (closestTop === undefined || (rect.top < 0 && rect.top > closestTop)){\n              closestTop = rect.top;\n              closest = headers[i];\n            }\n          }\n          return closest.innerHTML;\n        }\n\n        document.addEventListener('scroll', function(){\n          var percentage = Math.floor((100 * Math.max(0, pageYOffset)) / (document.body.scrollHeight - innerHeight)) + '%'\n          pointer.innerHTML = getSection() + ' - ' + percentage\n\n          pointer.classList.add('show');\n\n          if (hideTimeout)\n            clearTimeout(hideTimeout);\n\n          hideTimeout = setTimeout(function(){\n            pointer.classList.remove('show');\n          }, 1000);\n        });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/simple/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n        <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n        <link rel=\"stylesheet\" href=\"../common/css/style.css\" />\n    </head>\n    <body>\n        <div class=\"instructions\">Resize the page to see the Tether flip.</div>\n\n        <div class=\"element\"></div>\n        <div class=\"target\"></div>\n\n        <script src=\"../../dist/js/tether.js\"></script>\n        <script>\n            new Tether({\n                element: '.element',\n                target: '.target',\n                attachment: 'top left',\n                targetAttachment: 'top right',\n                constraints: [{\n                    to: 'window',\n                    attachment: 'together'\n                }]\n            });\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "examples/testbed/index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n        <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n        <link rel=\"stylesheet\" href=\"../common/css/style.css\" />\n    </head>\n    <body>\n\n        <div class=\"element\">\n        </div>\n\n        <div class=\"container\">\n            <div class=\"pad\"></div>\n            <div class=\"target\"></div>\n            <div class=\"pad\"></div>\n            <div class=\"pad\"></div>\n        </div>\n\n        <script src=\"../../dist/js/tether.js\"></script>\n        <script>\n            new Tether({\n                element: '.element',\n                target: '.target',\n                attachment: 'top center',\n                targetAttachment: 'bottom center',\n                constraints: [{\n                  to: 'scrollParent',\n                  attachment: 'together'\n                }]\n            });\n        </script>\n    </body>\n  </html>\n"
  },
  {
    "path": "examples/viewport/colors.css",
    "content": "@charset \"UTF-8\";\n/****\n\n   colors.css v1.0 For a friendlier looking web\n   MIT License • http://clrs.cc • http://github.com/mrmrs/colors\n\n   Author: mrmrs\n           http://mrmrs.cc\n           @mrmrs_\n\n****/\n/*\n\n   SKINS\n   • Backgrounds\n   • Colors\n\n*/\n/* Backgrounds */\n.bg-navy {\n  background-color: #001f3f; }\n\n.bg-blue {\n  background-color: #0074d9; }\n\n.bg-aqua {\n  background-color: #7fdbff; }\n\n.bg-teal {\n  background-color: #39cccc; }\n\n.bg-olive {\n  background-color: #3d9970; }\n\n.bg-green {\n  background-color: #2ecc40; }\n\n.bg-lime {\n  background-color: #01ff70; }\n\n.bg-yellow {\n  background-color: #ffdc00; }\n\n.bg-orange {\n  background-color: #ff851b; }\n\n.bg-red {\n  background-color: #ff4136; }\n\n.bg-fuchsia {\n  background-color: #f012be; }\n\n.bg-purple {\n  background-color: #b10dc9; }\n\n.bg-maroon {\n  background-color: #85144b; }\n\n.bg-white {\n  background-color: white; }\n\n.bg-gray {\n  background-color: #aaaaaa; }\n\n.bg-silver {\n  background-color: #dddddd; }\n\n.bg-black {\n  background-color: #111111; }\n\n/* Colors */\n.navy {\n  color: #001f3f; }\n\n.blue {\n  color: #0074d9; }\n\n.aqua {\n  color: #7fdbff; }\n\n.teal {\n  color: #39cccc; }\n\n.olive {\n  color: #3d9970; }\n\n.green {\n  color: #2ecc40; }\n\n.lime {\n  color: #01ff70; }\n\n.yellow {\n  color: #ffdc00; }\n\n.orange {\n  color: #ff851b; }\n\n.red {\n  color: #ff4136; }\n\n.fuchsia {\n  color: #f012be; }\n\n.purple {\n  color: #b10dc9; }\n\n.maroon {\n  color: #85144b; }\n\n.white {\n  color: white; }\n\n.silver {\n  color: #dddddd; }\n\n.gray {\n  color: #aaaaaa; }\n\n.black {\n  color: #111111; }\n\n/* PRETTIER LINKS */\na {\n  text-decoration: none;\n  -webkit-transition: color .3s ease-in-out;\n  transition: color .3s ease-in-out; }\n\na:link {\n  color: #0074d9;\n  -webkit-transition: color .3s ease-in-out;\n  transition: color .3s ease-in-out; }\n\na:visited {\n  color: #b10dc9; }\n\na:hover {\n  color: #7fdbff;\n  -webkit-transition: color .3s ease-in-out;\n  transition: color .3s ease-in-out; }\n\na:active {\n  color: #ff851b;\n  -webkit-transition: color .3s ease-in-out;\n  transition: color .3s ease-in-out; }\n"
  },
  {
    "path": "examples/viewport/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"../resources/css/base.css\" />\n    <link rel=\"stylesheet\" href=\"./colors.css\" />\n    <style>\n      * {\n        box-sizing: border-box;\n      }\n\n      .element {\n        background-color: #FFDC00;\n        width: 80%;\n        max-width: 300px;\n        padding: 0 15px;\n        font-size: 20px;\n        box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3);\n      }\n\n      @media (max-width: 380px) {\n        .element {\n          font-size: 16px;\n        }\n      }\n\n      .bit {\n        width: 10vw;\n        height: 10vw;\n        float: left;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"element\">\n      <p>This element is tethered to the middle of the visible part of the body.</p>\n\n      <p>Inspect the element to see how Tether decided\n      to use <code>position: fixed</code>.</p>\n    </div>\n\n    <script src=\"../../dist/js/tether.js\"></script>\n    <script>\n      new Tether({\n        element: '.element',\n        target: document.body,\n        attachment: 'middle center',\n        targetAttachment: 'middle center',\n        targetModifier: 'visible'\n      });\n    </script>\n\n    <script>\n      // Random colors bit, don't mind this\n      colors = ['navy', 'blue', 'aqua', 'teal', 'olive', 'green', 'lime',\n        'yellow', 'orange', 'red', 'fuchsia', 'purple', 'maroon'];\n\n      curColors = null;\n      for(var i=300; i--;){\n        if (!curColors || !curColors.length)\n          curColors = colors.slice(0);\n\n        var bit = document.createElement('div')\n        var index = (Math.random() * curColors.length)|0;\n        bit.className = 'bit bg-' + curColors[index]\n        curColors.splice(index, 1);\n        document.body.appendChild(bit);\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "jest.config.js",
    "content": "// For a detailed explanation regarding each configuration property, visit:\n// https://jestjs.io/docs/en/configuration.html\n\nmodule.exports = {\n  // Automatically clear mock calls and instances between every test\n  clearMocks: true,\n\n  // An array of glob patterns indicating a set of files for which coverage information should be collected\n  collectCoverageFrom: ['src/js/**/*.js'],\n\n  // The directory where Jest should output its coverage files\n  coverageDirectory: 'coverage',\n\n  // An array of file extensions your modules use\n  moduleFileExtensions: ['js'],\n\n  moduleNameMapper: {\n    '.+\\\\.(css|styl|less|sass|scss)$': '<rootDir>/__mocks__/styleMock.js'\n  },\n\n  // The path to a module that runs some code to configure or set up the testing framework before each test\n  setupFilesAfterEnv: ['<rootDir>/test/unit/setupTests.js'],\n\n  testEnvironment: 'jsdom',\n\n  testPathIgnorePatterns: ['/node_modules/', '/test/cypress/'],\n\n  // A map from regular expressions to paths to transformers\n  transform: {\n    '^.+\\\\.js$': 'babel-jest'\n  },\n\n  // Transform sinon ESM module (handle pnpm structure)\n  transformIgnorePatterns: [\n    'node_modules/(?!(.pnpm|sinon))'\n  ]\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"tether\",\n  \"version\": \"3.0.2\",\n  \"description\": \"A client-side library to make absolutely positioned elements attach to elements in the page efficiently.\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/shipshapecode/tether.git\"\n  },\n  \"license\": \"MIT\",\n  \"maintainers\": [\n    \"Nicholas Hwang <nick.joosung.hwang@gmail.com>\",\n    \"Trevor Burnham <trevorburnham@gmail.com>\"\n  ],\n  \"main\": \"dist/js/tether.js\",\n  \"module\": \"dist/js/tether.esm.js\",\n  \"scripts\": {\n    \"build\": \"pnpm clean && rollup -c\",\n    \"changelog\": \"github_changelog_generator -u shipshapecode -p tether --since-tag v1.4.7\",\n    \"clean\": \"rimraf dist\",\n    \"cy:open\": \"./node_modules/.bin/cypress open\",\n    \"cy:run\": \"./node_modules/.bin/cypress run\",\n    \"lint:js\": \"eslint . --ext js\",\n    \"prepare\": \"pnpm build\",\n    \"start\": \"pnpm watch\",\n    \"start-test-server\": \"http-server -p 9002\",\n    \"test\": \"pnpm lint:js && pnpm test:ci\",\n    \"test:ci\": \"pnpm test:unit:ci && pnpm test:cy:ci\",\n    \"test:cy:ci\": \"pnpm build && start-server-and-test start-test-server http://127.0.0.1:9002 cy:run\",\n    \"test:cy:watch\": \"pnpm build && start-server-and-test start-test-server http://127.0.0.1:9002 cy:open\",\n    \"test:unit:ci\": \"jest --coverage\",\n    \"test:unit:watch\": \"jest --watch\",\n    \"watch\": \"pnpm clean && rollup -c --environment DEVELOPMENT --watch\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.28.6\",\n    \"@babel/preset-env\": \"^7.29.0\",\n    \"@testing-library/jest-dom\": \"^5.17.0\",\n    \"autoprefixer\": \"^10.4.24\",\n    \"babel-jest\": \"^29.7.0\",\n    \"babel-plugin-rewire\": \"^1.2.0\",\n    \"babel-plugin-transform-es2015-modules-commonjs\": \"^6.26.2\",\n    \"chai\": \"^6.2.2\",\n    \"cssnano\": \"^7.1.2\",\n    \"cypress\": \"15.9.0\",\n    \"eslint\": \"^8.57.1\",\n    \"eslint-plugin-jest\": \"^29.12.1\",\n    \"http-server\": \"^14.1.1\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^30.2.0\",\n    \"jest-expect-message\": \"^1.1.3\",\n    \"jsdom\": \"^28.0.0\",\n    \"mutationobserver-shim\": \"^0.3.7\",\n    \"postcss\": \"^8.5.6\",\n    \"release-plan\": \"^0.17.4\",\n    \"rimraf\": \"^6.1.2\",\n    \"rollup\": \"^2.79.2\",\n    \"rollup-plugin-babel\": \"^4.4.0\",\n    \"rollup-plugin-browsersync\": \"^1.3.3\",\n    \"rollup-plugin-eslint\": \"^7.0.0\",\n    \"rollup-plugin-filesize\": \"^10.0.0\",\n    \"rollup-plugin-license\": \"^3.6.0\",\n    \"rollup-plugin-sass\": \"^1.15.3\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"rollup-plugin-visualizer\": \"^6.0.5\",\n    \"sinon\": \"^21.0.1\",\n    \"start-server-and-test\": \"^2.1.3\"\n  },\n  \"engines\": {\n    \"node\": \">= 20\",\n    \"pnpm\": \">= 10\"\n  },\n  \"publishConfig\": {\n    \"registry\": \"https://registry.npmjs.org\"\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"@parcel/watcher\",\n      \"core-js\",\n      \"cypress\"\n    ]\n  },\n  \"authors\": [\n    \"Zack Bloom <zackbloom@gmail.com>\",\n    \"Adam Schwartz <adam.flynn.schwartz@gmail.com>\"\n  ]\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import autoprefixer from 'autoprefixer';\nimport babel from 'rollup-plugin-babel';\nimport browsersync from 'rollup-plugin-browsersync';\nimport cssnano from 'cssnano';\nimport { eslint } from 'rollup-plugin-eslint';\nimport filesize from 'rollup-plugin-filesize';\nimport license from 'rollup-plugin-license';\nimport postcss from 'postcss';\nimport sass from 'rollup-plugin-sass';\nimport { terser } from 'rollup-plugin-terser';\nimport visualizer from 'rollup-plugin-visualizer';\nimport fs from 'fs';\n\nconst pkg = require('./package.json');\nconst banner = ['/*!', pkg.name, pkg.version, '*/\\n'].join(' ');\n\nconst env = process.env.DEVELOPMENT ? 'development' : 'production';\n\nfunction getSassOptions(minify = false) {\n  const postcssPlugins = [\n    autoprefixer({\n      grid: false\n    })\n  ];\n\n  if (minify) {\n    postcssPlugins.push(cssnano());\n  }\n  return {\n    output(styles, styleNodes) {\n      fs.mkdirSync('dist/css', { recursive: true }, (err) => {\n        if (err) {\n          throw err;\n        }\n      });\n\n      styleNodes.forEach(({ id, content }) => {\n        const scssName = id.substring(id.lastIndexOf('/') + 1, id.length);\n        const [name] = scssName.split('.');\n        fs.writeFileSync(`dist/css/${name}.${minify ? 'min.css' : 'css'}`, content);\n      });\n    },\n    processor: (css) => postcss(postcssPlugins)\n      .process(css, {\n        from: undefined\n      })\n      .then((result) => result.css)\n  };\n}\n\nconst plugins = [\n  eslint({\n    include: '**/*.js'\n  }),\n  babel(),\n  sass(getSassOptions(false)),\n  license({\n    banner\n  }),\n  filesize(),\n  visualizer()\n];\n\n// If we are running with --environment DEVELOPMENT, serve via browsersync for local development\nif (process.env.DEVELOPMENT) {\n  plugins.push(\n    browsersync({\n      host: 'localhost',\n      watch: true,\n      port: 3000,\n      notify: false,\n      open: true,\n      server: {\n        baseDir: 'examples',\n        routes: {\n          '/dist/js/tether.js': 'dist/js/tether.js',\n          '/dist/css/tether-theme-arrows-dark.css':\n            'dist/css/tether-theme-arrows-dark.css'\n        }\n      }\n    })\n  );\n}\n\nconst rollupBuilds = [\n  {\n    input: './src/js/tether.js',\n    output: [\n      {\n        file: pkg.main,\n        format: 'umd',\n        name: 'Tether',\n        sourcemap: true\n      },\n      {\n        file: pkg.module,\n        format: 'esm',\n        sourcemap: true\n      }\n    ],\n    plugins\n  }\n];\n\nrollupBuilds.push({\n  input: './src/js/tether.js',\n  output: [\n    {\n      file: 'dist/js/tether.min.js',\n      format: 'umd',\n      name: 'Tether',\n      sourcemap: true\n    },\n    {\n      file: 'dist/js/tether.esm.min.js',\n      format: 'esm',\n      sourcemap: true\n    }\n  ],\n  plugins: [\n    babel(),\n    sass(getSassOptions(true)),\n    terser(),\n    license({\n      banner\n    }),\n    filesize(),\n    visualizer()\n  ]\n});\n\nexport default rollupBuilds;\n"
  },
  {
    "path": "src/css/helpers/_tether-theme-arrows.scss",
    "content": "@mixin tether-theme-arrows($themePrefix: \"tether\", $themeName: \"arrows\", $arrowSize: 16px, $arrowPointerEvents: null, $backgroundColor: #fff, $color: inherit, $useDropShadow: false) {\n  .#{$themePrefix}-element.#{$themePrefix}-theme-#{$themeName} {\n    max-width: 100%;\n    max-height: 100%;\n\n    .#{$themePrefix}-content {\n      border-radius: 5px;\n      position: relative;\n      font-family: inherit;\n      background: $backgroundColor;\n      color: $color;\n      padding: 1em;\n      font-size: 1.1em;\n      line-height: 1.5em;\n\n      @if $useDropShadow {\n        transform: translateZ(0);\n        filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));\n      }\n\n      &:before {\n        content: \"\";\n        display: block;\n        position: absolute;\n        width: 0;\n        height: 0;\n        border-color: transparent;\n        border-width: $arrowSize;\n        border-style: solid;\n        pointer-events: $arrowPointerEvents;\n      }\n    }\n\n    // Centers and middles\n\n    &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center .#{$themePrefix}-content {\n      margin-bottom: $arrowSize;\n\n      &:before {\n        top: 100%;\n        left: 50%;\n        margin-left: -$arrowSize;\n        border-top-color: $backgroundColor;\n        border-bottom: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center .#{$themePrefix}-content {\n      margin-top: $arrowSize;\n\n      &:before {\n        bottom: 100%;\n        left: 50%;\n        margin-left: -$arrowSize;\n        border-bottom-color: $backgroundColor;\n        border-top: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content {\n      margin-right: $arrowSize;\n\n      &:before {\n        left: 100%;\n        top: 50%;\n        margin-top: -$arrowSize;\n        border-left-color: $backgroundColor;\n        border-right: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content {\n      margin-left: $arrowSize;\n\n      &:before {\n        right: 100%;\n        top: 50%;\n        margin-top: -$arrowSize;\n        border-right-color: $backgroundColor;\n        border-left: 0;\n      }\n    }\n\n    // Target middle/center, element corner\n\n    &.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-center .#{$themePrefix}-content {\n      left: -$arrowSize * 2;\n    }\n\n    &.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-center .#{$themePrefix}-content {\n      left: $arrowSize * 2;\n    }\n\n    &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content {\n      margin-top: $arrowSize;\n\n      &:before {\n        bottom: 100%;\n        left: $arrowSize;\n        border-bottom-color: $backgroundColor;\n        border-top: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content {\n      margin-top: $arrowSize;\n\n      &:before {\n        bottom: 100%;\n        right: $arrowSize;\n        border-bottom-color: $backgroundColor;\n        border-top: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content {\n      margin-bottom: $arrowSize;\n\n      &:before {\n        top: 100%;\n        left: $arrowSize;\n        border-top-color: $backgroundColor;\n        border-bottom: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content {\n      margin-bottom: $arrowSize;\n\n      &:before {\n        top: 100%;\n        right: $arrowSize;\n        border-top-color: $backgroundColor;\n        border-bottom: 0;\n      }\n    }\n\n    // Top and bottom corners\n\n    &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content {\n      margin-top: $arrowSize;\n\n      &:before {\n        bottom: 100%;\n        left: $arrowSize;\n        border-bottom-color: $backgroundColor;\n        border-top: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content {\n      margin-top: $arrowSize;\n\n      &:before {\n        bottom: 100%;\n        right: $arrowSize;\n        border-bottom-color: $backgroundColor;\n        border-top: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-top .#{$themePrefix}-content {\n      margin-bottom: $arrowSize;\n\n      &:before {\n        top: 100%;\n        left: $arrowSize;\n        border-top-color: $backgroundColor;\n        border-bottom: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-top .#{$themePrefix}-content {\n      margin-bottom: $arrowSize;\n\n      &:before {\n        top: 100%;\n        right: $arrowSize;\n        border-top-color: $backgroundColor;\n        border-bottom: 0;\n      }\n    }\n\n    // Side corners\n\n    &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content {\n      margin-right: $arrowSize;\n\n      &:before {\n        top: $arrowSize;\n        left: 100%;\n        border-left-color: $backgroundColor;\n        border-right: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content {\n      margin-left: $arrowSize;\n\n      &:before {\n        top: $arrowSize;\n        right: 100%;\n        border-right-color: $backgroundColor;\n        border-left: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content {\n      margin-right: $arrowSize;\n\n      &:before {\n        bottom: $arrowSize;\n        left: 100%;\n        border-left-color: $backgroundColor;\n        border-right: 0;\n      }\n    }\n\n    &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content {\n      margin-left: $arrowSize;\n\n      &:before {\n        bottom: $arrowSize;\n        right: 100%;\n        border-right-color: $backgroundColor;\n        border-left: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/css/helpers/_tether-theme-basic.scss",
    "content": "@mixin tether-theme-basic($themePrefix: \"tether\", $themeName: \"basic\", $backgroundColor: #fff, $color: inherit) {\n  .#{$themePrefix}-element.#{$themePrefix}-theme-#{$themeName} {\n    max-width: 100%;\n    max-height: 100%;\n\n    .#{$themePrefix}-content {\n      border-radius: 5px;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n      font-family: inherit;\n      background: $backgroundColor;\n      color: $color;\n      padding: 1em;\n      font-size: 1.1em;\n      line-height: 1.5em;\n    }\n  }\n}\n"
  },
  {
    "path": "src/css/helpers/_tether.scss",
    "content": "@mixin tether($themePrefix: \"tether\") {\n  .#{$themePrefix}-element, .#{$themePrefix}-element * {\n    &, &:after, &:before {\n      box-sizing: border-box;\n    }\n  }\n\n  .#{$themePrefix}-element {\n    position: absolute;\n    display: none;\n\n    &.#{$themePrefix}-open {\n      display: block;\n    }\n  }\n}\n"
  },
  {
    "path": "src/css/mixins/_inline-block.scss",
    "content": "@mixin inline-block {\n  display: inline-block;\n  vertical-align: middle;\n  *vertical-align: auto;\n  *zoom: 1;\n  *display: inline;\n}\n"
  },
  {
    "path": "src/css/mixins/_pie-clearfix.scss",
    "content": "@mixin pie-clearfix {\n  *zoom: 1;\n\n  &:after {\n    content: \"\";\n    display: table;\n    clear: both;\n  }\n}\n"
  },
  {
    "path": "src/css/tether-theme-arrows-dark.scss",
    "content": "@import \"helpers/tether\";\n@import \"helpers/tether-theme-arrows\";\n\n$themePrefix: \"tether\";\n$themeName: \"arrows-dark\";\n$arrowSize: 16px;\n$backgroundColor: #000;\n$color: #fff;\n$useDropShadow: false;\n\n@include tether($themePrefix: $themePrefix);\n@include tether-theme-arrows($themePrefix: $themePrefix, $themeName: $themeName, $arrowSize: $arrowSize, $backgroundColor: $backgroundColor, $color: $color, $useDropShadow: $useDropShadow);\n"
  },
  {
    "path": "src/css/tether-theme-arrows.scss",
    "content": "@import \"helpers/tether\";\n@import \"helpers/tether-theme-arrows\";\n\n$themePrefix: \"tether\";\n$themeName: \"arrows\";\n$arrowSize: 16px;\n$backgroundColor: #fff;\n$color: inherit;\n$useDropShadow: true;\n\n@include tether($themePrefix: $themePrefix);\n@include tether-theme-arrows($themePrefix: $themePrefix, $themeName: $themeName, $arrowSize: $arrowSize, $backgroundColor: $backgroundColor, $color: $color, $useDropShadow: $useDropShadow);\n"
  },
  {
    "path": "src/css/tether-theme-basic.scss",
    "content": "@import \"helpers/tether\";\n@import \"helpers/tether-theme-basic\";\n\n$themePrefix: \"tether\";\n$themeName: \"basic\";\n$backgroundColor: #fff;\n$color: inherit;\n\n@include tether($themePrefix: $themePrefix);\n@include tether-theme-basic($themePrefix: $themePrefix, $themeName: $themeName, $backgroundColor: $backgroundColor, $color: $color);\n"
  },
  {
    "path": "src/css/tether.scss",
    "content": "@import \"helpers/tether\";\n\n$themePrefix: \"tether\";\n\n@include tether($themePrefix: $themePrefix);\n"
  },
  {
    "path": "src/js/abutment.js",
    "content": "import { getClass, updateClasses } from './utils/classes';\nimport { defer } from './utils/deferred';\nimport { getBounds } from './utils/bounds';\n\nexport default {\n  position({ top, left }) {\n    const { height, width } = this.cache('element-bounds', () => {\n      return getBounds(this.element);\n    });\n\n    const targetPos = this.getTargetBounds();\n\n    const bottom = top + height;\n    const right = left + width;\n\n    const abutted = [];\n    if (top <= targetPos.bottom && bottom >= targetPos.top) {\n      ['left', 'right'].forEach((side) => {\n        const targetPosSide = targetPos[side];\n        if (targetPosSide === left || targetPosSide === right) {\n          abutted.push(side);\n        }\n      });\n    }\n\n    if (left <= targetPos.right && right >= targetPos.left) {\n      ['top', 'bottom'].forEach((side) => {\n        const targetPosSide = targetPos[side];\n        if (targetPosSide === top || targetPosSide === bottom) {\n          abutted.push(side);\n        }\n      });\n    }\n\n    const sides = ['left', 'top', 'right', 'bottom'];\n    const { classes, classPrefix } = this.options;\n    this.all.push(getClass('abutted', classes, classPrefix));\n    sides.forEach((side) => {\n      this.all.push(`${getClass('abutted', classes, classPrefix)}-${side}`);\n    });\n\n    if (abutted.length) {\n      this.add.push(getClass('abutted', classes, classPrefix));\n    }\n\n    abutted.forEach((side) => {\n      this.add.push(`${getClass('abutted', classes, classPrefix)}-${side}`);\n    });\n\n    defer(() => {\n      if (!(this.options.addTargetClasses === false)) {\n        updateClasses(this.target, this.add, this.all);\n      }\n      updateClasses(this.element, this.add, this.all);\n    });\n\n    return true;\n  }\n};\n"
  },
  {
    "path": "src/js/constraint.js",
    "content": "import { getClass, updateClasses } from './utils/classes';\nimport { defer } from './utils/deferred';\nimport { extend } from './utils/general';\nimport { getBounds } from './utils/bounds';\nimport { isString, isUndefined } from './utils/type-check';\n\nconst BOUNDS_FORMAT = ['left', 'top', 'right', 'bottom'];\n\n/**\n * Returns an array of bounds of the format [left, top, right, bottom]\n * @param tether\n * @param to\n * @return {*[]|HTMLElement|ActiveX.IXMLDOMElement}\n */\nfunction getBoundingRect(body, tether, to) {\n  // arg to is required\n  if (!to) {\n    return null;\n  }\n  if (to === 'scrollParent') {\n    to = tether.scrollParents[0];\n  } else if (to === 'window') {\n    to = [pageXOffset, pageYOffset, innerWidth + pageXOffset, innerHeight + pageYOffset];\n  }\n\n  if (to === document) {\n    to = to.documentElement;\n  }\n\n  if (!isUndefined(to.nodeType)) {\n    const node = to;\n    const size = getBounds(body, to);\n    const pos = size;\n    const style = getComputedStyle(to);\n\n    to = [pos.left, pos.top, size.width + pos.left, size.height + pos.top];\n\n    // Account any parent Frames scroll offset\n    if (node.ownerDocument !== document) {\n      let win = node.ownerDocument.defaultView;\n      to[0] += win.pageXOffset;\n      to[1] += win.pageYOffset;\n      to[2] += win.pageXOffset;\n      to[3] += win.pageYOffset;\n    }\n\n    BOUNDS_FORMAT.forEach((side, i) => {\n      side = side[0].toUpperCase() + side.substr(1);\n      if (side === 'Top' || side === 'Left') {\n        to[i] += parseFloat(style[`border${side}Width`]);\n      } else {\n        to[i] -= parseFloat(style[`border${side}Width`]);\n      }\n    });\n  }\n\n  return to;\n}\n\n/**\n * Add out of bounds classes to the list of classes we add to tether\n * @param {string[]} oob An array of directions that are out of bounds\n * @param {string[]} addClasses The array of classes to add to Tether\n * @param {string[]} classes The array of class types for Tether\n * @param {string} classPrefix The prefix to add to the front of the class\n * @param {string} outOfBoundsClass The class to apply when out of bounds\n * @private\n */\nfunction _addOutOfBoundsClass(oob, addClasses, classes, classPrefix, outOfBoundsClass) {\n  if (oob.length) {\n    let oobClass;\n    if (!isUndefined(outOfBoundsClass)) {\n      oobClass = outOfBoundsClass;\n    } else {\n      oobClass = getClass('out-of-bounds', classes, classPrefix);\n    }\n\n    addClasses.push(oobClass);\n    oob.forEach((side) => {\n      addClasses.push(`${oobClass}-${side}`);\n    });\n  }\n}\n\n/**\n * Calculates if out of bounds or pinned in the X direction.\n *\n * @param {number} left\n * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom]\n * @param {number} width\n * @param pin\n * @param pinned\n * @param {string[]} oob\n * @return {number}\n * @private\n */\nfunction _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob) {\n  if (left < bounds[0]) {\n    if (pin.indexOf('left') >= 0) {\n      left = bounds[0];\n      pinned.push('left');\n    } else {\n      oob.push('left');\n    }\n  }\n\n  if (left + width > bounds[2]) {\n    if (pin.indexOf('right') >= 0) {\n      left = bounds[2] - width;\n      pinned.push('right');\n    } else {\n      oob.push('right');\n    }\n  }\n\n  return left;\n}\n\n/**\n * Calculates if out of bounds or pinned in the Y direction.\n *\n * @param {number} top\n * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom]\n * @param {number} height\n * @param pin\n * @param {string[]} pinned\n * @param {string[]} oob\n * @return {number}\n * @private\n */\nfunction _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob) {\n  if (top < bounds[1]) {\n    if (pin.indexOf('top') >= 0) {\n      top = bounds[1];\n      pinned.push('top');\n    } else {\n      oob.push('top');\n    }\n  }\n\n  if (top + height > bounds[3]) {\n    if (pin.indexOf('bottom') >= 0) {\n      top = bounds[3] - height;\n      pinned.push('bottom');\n    } else {\n      oob.push('bottom');\n    }\n  }\n\n  return top;\n}\n\n/**\n * Flip X \"together\"\n * @param {object} tAttachment The target attachment\n * @param {object} eAttachment The element attachment\n * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom]\n * @param {number} width\n * @param targetWidth\n * @param {number} left\n * @private\n */\nfunction _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left) {\n  if (left < bounds[0] && tAttachment.left === 'left') {\n    if (eAttachment.left === 'right') {\n      left += targetWidth;\n      tAttachment.left = 'right';\n\n      left += width;\n      eAttachment.left = 'left';\n\n    } else if (eAttachment.left === 'left') {\n      left += targetWidth;\n      tAttachment.left = 'right';\n\n      left -= width;\n      eAttachment.left = 'right';\n    }\n\n  } else if (left + width > bounds[2] && tAttachment.left === 'right') {\n    if (eAttachment.left === 'left') {\n      left -= targetWidth;\n      tAttachment.left = 'left';\n\n      left -= width;\n      eAttachment.left = 'right';\n\n    } else if (eAttachment.left === 'right') {\n      left -= targetWidth;\n      tAttachment.left = 'left';\n\n      left += width;\n      eAttachment.left = 'left';\n    }\n\n  } else if (tAttachment.left === 'center') {\n    if (left + width > bounds[2] && eAttachment.left === 'left') {\n      left -= width;\n      eAttachment.left = 'right';\n\n    } else if (left < bounds[0] && eAttachment.left === 'right') {\n      left += width;\n      eAttachment.left = 'left';\n    }\n  }\n\n  return left;\n}\n\n/**\n * Flip Y \"together\"\n * @param {object} tAttachment The target attachment\n * @param {object} eAttachment The element attachment\n * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom]\n * @param {number} height\n * @param targetHeight\n * @param {number} top\n * @private\n */\nfunction _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top) {\n  if (tAttachment.top === 'top') {\n    if (eAttachment.top === 'bottom' && top < bounds[1]) {\n      top += targetHeight;\n      tAttachment.top = 'bottom';\n\n      top += height;\n      eAttachment.top = 'top';\n\n    } else if (eAttachment.top === 'top' && top + height > bounds[3] && top - (height - targetHeight) >= bounds[1]) {\n      top -= height - targetHeight;\n      tAttachment.top = 'bottom';\n\n      eAttachment.top = 'bottom';\n    }\n  }\n\n  if (tAttachment.top === 'bottom') {\n    if (eAttachment.top === 'top' && top + height > bounds[3]) {\n      top -= targetHeight;\n      tAttachment.top = 'top';\n\n      top -= height;\n      eAttachment.top = 'bottom';\n\n    } else if (eAttachment.top === 'bottom' && top < bounds[1] && top + (height * 2 - targetHeight) <= bounds[3]) {\n      top += height - targetHeight;\n      tAttachment.top = 'top';\n\n      eAttachment.top = 'top';\n\n    }\n  }\n\n  if (tAttachment.top === 'middle') {\n    if (top + height > bounds[3] && eAttachment.top === 'top') {\n      top -= height;\n      eAttachment.top = 'bottom';\n\n    } else if (top < bounds[1] && eAttachment.top === 'bottom') {\n      top += height;\n      eAttachment.top = 'top';\n    }\n  }\n\n  return top;\n}\n\n/**\n * Get all the initial classes\n * @param classes\n * @param {string} classPrefix\n * @param constraints\n * @return {[*, *]}\n * @private\n */\nfunction _getAllClasses(classes, classPrefix, constraints) {\n  const allClasses = [getClass('pinned', classes, classPrefix), getClass('out-of-bounds', classes, classPrefix)];\n\n  constraints.forEach((constraint) => {\n    const { outOfBoundsClass, pinnedClass } = constraint;\n    if (outOfBoundsClass) {\n      allClasses.push(outOfBoundsClass);\n    }\n    if (pinnedClass) {\n      allClasses.push(pinnedClass);\n    }\n  });\n\n  allClasses.forEach((cls) => {\n    ['left', 'top', 'right', 'bottom'].forEach((side) => {\n      allClasses.push(`${cls}-${side}`);\n    });\n  });\n\n  return allClasses;\n}\n\nexport default {\n  position({ top, left, targetAttachment }) {\n    if (!this.options.constraints) {\n      return true;\n    }\n\n    let { height, width } = this.cache('element-bounds', () => {\n      return getBounds(this.bodyElement, this.element);\n    });\n\n    if (width === 0 && height === 0 && !isUndefined(this.lastSize)) {\n      // Handle the item getting hidden as a result of our positioning without glitching\n      // the classes in and out\n      ({ width, height } = this.lastSize);\n    }\n\n    const targetSize = this.cache('target-bounds', () => {\n      return this.getTargetBounds();\n    });\n\n    const { height: targetHeight, width: targetWidth } = targetSize;\n    const { classes, classPrefix } = this.options;\n\n    const allClasses = _getAllClasses(classes, classPrefix, this.options.constraints);\n    const addClasses = [];\n\n    const tAttachment = extend({}, targetAttachment);\n    const eAttachment = extend({}, this.attachment);\n\n    this.options.constraints.forEach((constraint) => {\n      let { to, attachment, pin } = constraint;\n\n      if (isUndefined(attachment)) {\n        attachment = '';\n      }\n\n      let changeAttachX, changeAttachY;\n      if (attachment.indexOf(' ') >= 0) {\n        [changeAttachY, changeAttachX] = attachment.split(' ');\n      } else {\n        changeAttachX = changeAttachY = attachment;\n      }\n\n      const bounds = getBoundingRect(this.bodyElement, this, to);\n\n      if (changeAttachY === 'target' || changeAttachY === 'both') {\n        if (top < bounds[1] && tAttachment.top === 'top') {\n          top += targetHeight;\n          tAttachment.top = 'bottom';\n        }\n\n        if (top + height > bounds[3] && tAttachment.top === 'bottom') {\n          top -= targetHeight;\n          tAttachment.top = 'top';\n        }\n      }\n\n      if (changeAttachY === 'together') {\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n      }\n\n      if (changeAttachX === 'target' || changeAttachX === 'both') {\n        if (left < bounds[0] && tAttachment.left === 'left') {\n          left += targetWidth;\n          tAttachment.left = 'right';\n        }\n\n        if (left + width > bounds[2] && tAttachment.left === 'right') {\n          left -= targetWidth;\n          tAttachment.left = 'left';\n        }\n      }\n\n      if (changeAttachX === 'together') {\n        left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left);\n      }\n\n      if (changeAttachY === 'element' || changeAttachY === 'both') {\n        if (top < bounds[1] && eAttachment.top === 'bottom') {\n          top += height;\n          eAttachment.top = 'top';\n        }\n\n        if (top + height > bounds[3] && eAttachment.top === 'top') {\n          top -= height;\n          eAttachment.top = 'bottom';\n        }\n      }\n\n      if (changeAttachX === 'element' || changeAttachX === 'both') {\n        if (left < bounds[0]) {\n          if (eAttachment.left === 'right') {\n            left += width;\n            eAttachment.left = 'left';\n          } else if (eAttachment.left === 'center') {\n            left += (width / 2);\n            eAttachment.left = 'left';\n          }\n        }\n\n        if (left + width > bounds[2]) {\n          if (eAttachment.left === 'left') {\n            left -= width;\n            eAttachment.left = 'right';\n          } else if (eAttachment.left === 'center') {\n            left -= (width / 2);\n            eAttachment.left = 'right';\n          }\n        }\n      }\n\n      if (isString(pin)) {\n        pin = pin.split(',').map((p) => p.trim());\n      } else if (pin === true) {\n        pin = ['top', 'left', 'right', 'bottom'];\n      }\n\n      pin = pin || [];\n\n      const pinned = [];\n      const oob = [];\n\n      left = _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob);\n      top = _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob);\n\n      if (pinned.length) {\n        let pinnedClass;\n        if (!isUndefined(this.options.pinnedClass)) {\n          pinnedClass = this.options.pinnedClass;\n        } else {\n          pinnedClass = getClass('pinned', classes, classPrefix);\n        }\n\n        addClasses.push(pinnedClass);\n        pinned.forEach((side) => {\n          addClasses.push(`${pinnedClass}-${side}`);\n        });\n      }\n\n      _addOutOfBoundsClass(oob, addClasses, classes, classPrefix, this.options.outOfBoundsClass);\n\n      if (pinned.indexOf('left') >= 0 || pinned.indexOf('right') >= 0) {\n        eAttachment.left = tAttachment.left = false;\n      }\n      if (pinned.indexOf('top') >= 0 || pinned.indexOf('bottom') >= 0) {\n        eAttachment.top = tAttachment.top = false;\n      }\n\n      if (tAttachment.top !== targetAttachment.top ||\n        tAttachment.left !== targetAttachment.left ||\n        eAttachment.top !== this.attachment.top ||\n        eAttachment.left !== this.attachment.left) {\n        this.updateAttachClasses(eAttachment, tAttachment);\n        this.trigger('update', {\n          attachment: eAttachment,\n          targetAttachment: tAttachment\n        });\n      }\n    });\n\n    defer(() => {\n      if (!(this.options.addTargetClasses === false)) {\n        updateClasses(this.target, addClasses, allClasses);\n      }\n      updateClasses(this.element, addClasses, allClasses);\n    });\n\n    return { top, left };\n  }\n};\n"
  },
  {
    "path": "src/js/evented.js",
    "content": "import { isUndefined } from './utils/type-check';\n\nexport class Evented {\n  on(event, handler, ctx, once = false) {\n    if (isUndefined(this.bindings)) {\n      this.bindings = {};\n    }\n    if (isUndefined(this.bindings[event])) {\n      this.bindings[event] = [];\n    }\n    this.bindings[event].push({ handler, ctx, once });\n\n    return this;\n  }\n\n  once(event, handler, ctx) {\n    return this.on(event, handler, ctx, true);\n  }\n\n  off(event, handler) {\n    if (isUndefined(this.bindings) ||\n      isUndefined(this.bindings[event])) {\n      return this;\n    }\n\n    if (isUndefined(handler)) {\n      delete this.bindings[event];\n    } else {\n      this.bindings[event].forEach((binding, index) => {\n        if (binding.handler === handler) {\n          this.bindings[event].splice(index, 1);\n        }\n      });\n    }\n\n    return this;\n  }\n\n  trigger(event, ...args) {\n    if (!isUndefined(this.bindings) && this.bindings[event]) {\n      this.bindings[event].forEach((binding, index) => {\n        const { ctx, handler, once } = binding;\n\n        const context = ctx || this;\n\n        handler.apply(context, args);\n\n        if (once) {\n          this.bindings[event].splice(index, 1);\n        }\n      });\n    }\n\n    return this;\n  }\n}\n"
  },
  {
    "path": "src/js/shift.js",
    "content": "import { isFunction, isString } from './utils/type-check';\n\nexport default {\n  position({ top, left }) {\n    if (!this.options.shift) {\n      return;\n    }\n\n    let { shift } = this.options;\n    if (isFunction(shift)) {\n      shift = shift.call(this, { top, left });\n    }\n\n    let shiftTop, shiftLeft;\n    if (isString(shift)) {\n      shift = shift.split(' ');\n      shift[1] = shift[1] || shift[0];\n\n      ([shiftTop, shiftLeft] = shift);\n\n      shiftTop = parseFloat(shiftTop, 10);\n      shiftLeft = parseFloat(shiftLeft, 10);\n    } else {\n      ([shiftTop, shiftLeft] = [shift.top, shift.left]);\n    }\n\n    top += shiftTop;\n    left += shiftLeft;\n\n    return { top, left };\n  }\n};\n"
  },
  {
    "path": "src/js/tether.js",
    "content": "import '../css/tether.scss';\nimport '../css/tether-theme-arrows.scss';\nimport '../css/tether-theme-arrows-dark.scss';\nimport '../css/tether-theme-basic.scss';\nimport Abutment from './abutment';\nimport Constraint from './constraint';\nimport Shift from './shift';\nimport { Evented } from './evented';\nimport {\n  addClass,\n  getClass,\n  removeClass,\n  updateClasses\n} from './utils/classes';\nimport { defer, flush } from './utils/deferred';\nimport { extend, getScrollBarSize } from './utils/general';\nimport {\n  addOffset,\n  attachmentToOffset,\n  autoToFixedAttachment,\n  offsetToPx,\n  parseTopLeft\n} from './utils/offset';\nimport {\n  getBounds,\n  getScrollHandleBounds,\n  getVisibleBounds,\n  removeUtilElements\n} from './utils/bounds';\nimport { getOffsetParent, getScrollParents } from './utils/parents';\nimport { isNumber, isObject, isString, isUndefined } from './utils/type-check';\n\nconst TetherBase = { modules: [Constraint, Abutment, Shift] };\n\nfunction isFullscreenElement(e) {\n  let d = e.ownerDocument;\n  let fe =\n    d.fullscreenElement ||\n    d.webkitFullscreenElement ||\n    d.mozFullScreenElement ||\n    d.msFullscreenElement;\n  return fe === e;\n}\n\nfunction within(a, b, diff = 1) {\n  return a + diff >= b && b >= a - diff;\n}\n\nconst transformKey = (() => {\n  if (typeof document === 'undefined') {\n    return '';\n  }\n  const el = document.createElement('div');\n\n  const transforms = [\n    'transform',\n    'WebkitTransform',\n    'OTransform',\n    'MozTransform',\n    'msTransform'\n  ];\n  for (let i = 0; i < transforms.length; ++i) {\n    const key = transforms[i];\n    if (el.style[key] !== undefined) {\n      return key;\n    }\n  }\n})();\n\nconst tethers = [];\n\nconst position = () => {\n  tethers.forEach((tether) => {\n    tether.position(false);\n  });\n  flush();\n};\n\nfunction now() {\n  return performance.now();\n}\n\n(() => {\n  let lastCall = null;\n  let lastDuration = null;\n  let pendingTimeout = null;\n\n  const tick = () => {\n    if (!isUndefined(lastDuration) && lastDuration > 16) {\n      // We voluntarily throttle ourselves if we can't manage 60fps\n      lastDuration = Math.min(lastDuration - 16, 250);\n\n      // Just in case this is the last event, remember to position just once more\n      pendingTimeout = setTimeout(tick, 250);\n      return;\n    }\n\n    if (!isUndefined(lastCall) && now() - lastCall < 10) {\n      // Some browsers call events a little too frequently, refuse to run more than is reasonable\n      return;\n    }\n\n    if (pendingTimeout != null) {\n      clearTimeout(pendingTimeout);\n      pendingTimeout = null;\n    }\n\n    lastCall = now();\n    position();\n    lastDuration = now() - lastCall;\n  };\n\n  if (typeof window !== 'undefined' && !isUndefined(window.addEventListener)) {\n    ['resize', 'scroll', 'touchmove'].forEach((event) => {\n      window.addEventListener(event, tick);\n    });\n  }\n})();\n\nclass TetherClass extends Evented {\n  constructor(options) {\n    super();\n    this.position = this.position.bind(this);\n\n    tethers.push(this);\n\n    this.history = [];\n\n    this.setOptions(options, false);\n\n    TetherBase.modules.forEach((module) => {\n      if (!isUndefined(module.initialize)) {\n        module.initialize.call(this);\n      }\n    });\n\n    this.position();\n  }\n\n  setOptions(options, pos = true) {\n    const defaults = {\n      offset: '0 0',\n      targetOffset: '0 0',\n      targetAttachment: 'auto auto',\n      classPrefix: 'tether',\n      bodyElement: document.body\n    };\n\n    this.options = extend(defaults, options);\n\n    let { element, target, targetModifier, bodyElement } = this.options;\n    this.element = element;\n    this.target = target;\n    this.targetModifier = targetModifier;\n\n    if (typeof bodyElement === 'string') {\n      bodyElement = document.querySelector(bodyElement);\n    }\n    this.bodyElement = bodyElement;\n\n    if (this.target === 'viewport') {\n      this.target = document.body;\n      this.targetModifier = 'visible';\n    } else if (this.target === 'scroll-handle') {\n      this.target = document.body;\n      this.targetModifier = 'scroll-handle';\n    }\n\n    ['element', 'target'].forEach((key) => {\n      if (isUndefined(this[key])) {\n        throw new Error(\n          'Tether Error: Both element and target must be defined'\n        );\n      }\n\n      if (!isUndefined(this[key].jquery)) {\n        this[key] = this[key][0];\n      } else if (isString(this[key])) {\n        this[key] = document.querySelector(this[key]);\n      }\n    });\n\n    this._addClasses();\n\n    if (!this.options.attachment) {\n      throw new Error('Tether Error: You must provide an attachment');\n    }\n\n    this.targetAttachment = parseTopLeft(this.options.targetAttachment);\n    this.attachment = parseTopLeft(this.options.attachment);\n    this.offset = parseTopLeft(this.options.offset);\n    this.targetOffset = parseTopLeft(this.options.targetOffset);\n\n    if (!isUndefined(this.scrollParents)) {\n      this.disable();\n    }\n\n    if (this.targetModifier === 'scroll-handle') {\n      this.scrollParents = [this.target];\n    } else {\n      this.scrollParents = getScrollParents(this.target);\n    }\n\n    if (!(this.options.enabled === false)) {\n      this.enable(pos);\n    }\n  }\n\n  getTargetBounds() {\n    if (!isUndefined(this.targetModifier)) {\n      if (this.targetModifier === 'visible') {\n        return getVisibleBounds(this.bodyElement, this.target);\n      } else if (this.targetModifier === 'scroll-handle') {\n        return getScrollHandleBounds(this.bodyElement, this.target);\n      }\n    } else {\n      return getBounds(this.bodyElement, this.target);\n    }\n  }\n\n  clearCache() {\n    this._cache = {};\n  }\n\n  cache(k, getter) {\n    // More than one module will often need the same DOM info, so\n    // we keep a cache which is cleared on each position call\n    if (isUndefined(this._cache)) {\n      this._cache = {};\n    }\n\n    if (isUndefined(this._cache[k])) {\n      this._cache[k] = getter.call(this);\n    }\n\n    return this._cache[k];\n  }\n\n  enable(pos = true) {\n    const { classes, classPrefix } = this.options;\n    if (!(this.options.addTargetClasses === false)) {\n      addClass(this.target, getClass('enabled', classes, classPrefix));\n    }\n    addClass(this.element, getClass('enabled', classes, classPrefix));\n    this.enabled = true;\n\n    this.scrollParents.forEach((parent) => {\n      if (parent !== this.target.ownerDocument) {\n        parent.addEventListener('scroll', this.position);\n      }\n    });\n\n    if (pos) {\n      this.position();\n    }\n  }\n\n  disable() {\n    const { classes, classPrefix } = this.options;\n    removeClass(this.target, getClass('enabled', classes, classPrefix));\n    removeClass(this.element, getClass('enabled', classes, classPrefix));\n    this.enabled = false;\n\n    if (!isUndefined(this.scrollParents)) {\n      this.scrollParents.forEach((parent) => {\n        if (parent && parent.removeEventListener) {\n          parent.removeEventListener('scroll', this.position);\n        }\n      });\n    }\n  }\n\n  destroy() {\n    this.disable();\n\n    this._removeClasses();\n\n    TetherBase.modules.forEach((module) => {\n      if (!isUndefined(module.destroy)) {\n        module.destroy.call(this);\n      }\n    });\n\n    tethers.forEach((tether, i) => {\n      if (tether === this) {\n        tethers.splice(i, 1);\n      }\n    });\n\n    // Remove any elements we were using for convenience from the DOM\n    if (tethers.length === 0) {\n      removeUtilElements(this.bodyElement);\n    }\n  }\n\n  updateAttachClasses(elementAttach, targetAttach) {\n    elementAttach = elementAttach || this.attachment;\n    targetAttach = targetAttach || this.targetAttachment;\n    const sides = ['left', 'top', 'bottom', 'right', 'middle', 'center'];\n    const { classes, classPrefix } = this.options;\n\n    if (!isUndefined(this._addAttachClasses) && this._addAttachClasses.length) {\n      // updateAttachClasses can be called more than once in a position call, so\n      // we need to clean up after ourselves such that when the last defer gets\n      // ran it doesn't add any extra classes from previous calls.\n      this._addAttachClasses.splice(0, this._addAttachClasses.length);\n    }\n\n    if (isUndefined(this._addAttachClasses)) {\n      this._addAttachClasses = [];\n    }\n    this.add = this._addAttachClasses;\n\n    if (elementAttach.top) {\n      this.add.push(\n        `${getClass('element-attached', classes, classPrefix)}-${\n          elementAttach.top\n        }`\n      );\n    }\n    if (elementAttach.left) {\n      this.add.push(\n        `${getClass('element-attached', classes, classPrefix)}-${\n          elementAttach.left\n        }`\n      );\n    }\n    if (targetAttach.top) {\n      this.add.push(\n        `${getClass('target-attached', classes, classPrefix)}-${\n          targetAttach.top\n        }`\n      );\n    }\n    if (targetAttach.left) {\n      this.add.push(\n        `${getClass('target-attached', classes, classPrefix)}-${\n          targetAttach.left\n        }`\n      );\n    }\n\n    this.all = [];\n    sides.forEach((side) => {\n      this.all.push(\n        `${getClass('element-attached', classes, classPrefix)}-${side}`\n      );\n      this.all.push(\n        `${getClass('target-attached', classes, classPrefix)}-${side}`\n      );\n    });\n\n    defer(() => {\n      if (isUndefined(this._addAttachClasses)) {\n        return;\n      }\n\n      updateClasses(this.element, this._addAttachClasses, this.all);\n      if (!(this.options.addTargetClasses === false)) {\n        updateClasses(this.target, this._addAttachClasses, this.all);\n      }\n\n      delete this._addAttachClasses;\n    });\n  }\n\n  position(flushChanges = true) {\n    // flushChanges commits the changes immediately, leave true unless you are positioning multiple\n    // tethers (in which case call Tether.Utils.flush yourself when you're done)\n\n    if (!this.enabled) {\n      return;\n    }\n\n    this.clearCache();\n\n    // Turn 'auto' attachments into the appropriate corner or edge\n    const targetAttachment = autoToFixedAttachment(\n      this.targetAttachment,\n      this.attachment\n    );\n\n    this.updateAttachClasses(this.attachment, targetAttachment);\n\n    const elementPos = this.cache('element-bounds', () => {\n      return getBounds(this.bodyElement, this.element);\n    });\n\n    let { width, height } = elementPos;\n\n    if (width === 0 && height === 0 && !isUndefined(this.lastSize)) {\n      // We cache the height and width to make it possible to position elements that are\n      // getting hidden.\n      ({ width, height } = this.lastSize);\n    } else {\n      this.lastSize = { width, height };\n    }\n\n    const targetPos = this.cache('target-bounds', () => {\n      return this.getTargetBounds();\n    });\n    const targetSize = targetPos;\n\n    // Get an actual px offset from the attachment\n    let offset = offsetToPx(attachmentToOffset(this.attachment), {\n      width,\n      height\n    });\n    let targetOffset = offsetToPx(\n      attachmentToOffset(targetAttachment),\n      targetSize\n    );\n\n    const manualOffset = offsetToPx(this.offset, { width, height });\n    const manualTargetOffset = offsetToPx(this.targetOffset, targetSize);\n\n    // Add the manually provided offset\n    offset = addOffset(offset, manualOffset);\n    targetOffset = addOffset(targetOffset, manualTargetOffset);\n\n    // It's now our goal to make (element position + offset) == (target position + target offset)\n    let left = targetPos.left + targetOffset.left - offset.left;\n    let top = targetPos.top + targetOffset.top - offset.top;\n\n    for (let i = 0; i < TetherBase.modules.length; ++i) {\n      const module = TetherBase.modules[i];\n      const ret = module.position.call(this, {\n        left,\n        top,\n        targetAttachment,\n        targetPos,\n        elementPos,\n        offset,\n        targetOffset,\n        manualOffset,\n        manualTargetOffset,\n        scrollbarSize,\n        attachment: this.attachment\n      });\n\n      if (ret === false) {\n        return false;\n      } else if (isUndefined(ret) || !isObject(ret)) {\n        continue;\n      } else {\n        ({ top, left } = ret);\n      }\n    }\n\n    // We describe the position three different ways to give the optimizer\n    // a chance to decide the best possible way to position the element\n    // with the fewest repaints.\n    const next = {\n      // It's position relative to the page (absolute positioning when\n      // the element is a child of the body)\n      page: {\n        top,\n        left\n      },\n\n      // It's position relative to the viewport (fixed positioning)\n      viewport: {\n        top: top - pageYOffset,\n        bottom: pageYOffset - top - height + innerHeight,\n        left: left - pageXOffset,\n        right: pageXOffset - left - width + innerWidth\n      }\n    };\n\n    let doc = this.target.ownerDocument;\n    let win = doc.defaultView;\n\n    let scrollbarSize;\n    if (win.innerHeight > doc.documentElement.clientHeight) {\n      scrollbarSize = this.cache('scrollbar-size', getScrollBarSize);\n      next.viewport.bottom -= scrollbarSize.height;\n    }\n\n    if (win.innerWidth > doc.documentElement.clientWidth) {\n      scrollbarSize = this.cache('scrollbar-size', getScrollBarSize);\n      next.viewport.right -= scrollbarSize.width;\n    }\n\n    if (\n      ['', 'static'].indexOf(doc.body.style.position) === -1 ||\n      ['', 'static'].indexOf(doc.body.parentElement.style.position) === -1\n    ) {\n      // Absolute positioning in the body will be relative to the page, not the 'initial containing block'\n      next.page.bottom = doc.body.scrollHeight - top - height;\n      next.page.right = doc.body.scrollWidth - left - width;\n    }\n\n    if (\n      !isUndefined(this.options.optimizations) &&\n      this.options.optimizations.moveElement !== false &&\n      isUndefined(this.targetModifier)\n    ) {\n      const offsetParent = this.cache('target-offsetparent', () =>\n        getOffsetParent(this.target)\n      );\n      const offsetPosition = this.cache('target-offsetparent-bounds', () =>\n        getBounds(this.bodyElement, offsetParent)\n      );\n      const offsetParentStyle = getComputedStyle(offsetParent);\n      const offsetParentSize = offsetPosition;\n\n      const offsetBorder = {};\n      ['Top', 'Left', 'Bottom', 'Right'].forEach((side) => {\n        offsetBorder[side.toLowerCase()] = parseFloat(\n          offsetParentStyle[`border${side}Width`]\n        );\n      });\n\n      offsetPosition.right =\n        doc.body.scrollWidth -\n        offsetPosition.left -\n        offsetParentSize.width +\n        offsetBorder.right;\n      offsetPosition.bottom =\n        doc.body.scrollHeight -\n        offsetPosition.top -\n        offsetParentSize.height +\n        offsetBorder.bottom;\n\n      if (\n        next.page.top >= offsetPosition.top + offsetBorder.top &&\n        next.page.bottom >= offsetPosition.bottom\n      ) {\n        if (\n          next.page.left >= offsetPosition.left + offsetBorder.left &&\n          next.page.right >= offsetPosition.right\n        ) {\n          // We're within the visible part of the target's scroll parent\n          const { scrollLeft, scrollTop } = offsetParent;\n\n          // It's position relative to the target's offset parent (absolute positioning when\n          // the element is moved to be a child of the target's offset parent).\n          next.offset = {\n            top:\n              next.page.top - offsetPosition.top + scrollTop - offsetBorder.top,\n            left:\n              next.page.left -\n              offsetPosition.left +\n              scrollLeft -\n              offsetBorder.left\n          };\n        }\n      }\n    }\n\n    // We could also travel up the DOM and try each containing context, rather than only\n    // looking at the body, but we're gonna get diminishing returns.\n\n    this.move(next);\n\n    this.history.unshift(next);\n\n    if (this.history.length > 3) {\n      this.history.pop();\n    }\n\n    if (flushChanges) {\n      flush();\n    }\n\n    return true;\n  }\n\n  // THE ISSUE\n  move(pos) {\n    if (isUndefined(this.element.parentNode)) {\n      return;\n    }\n\n    const same = {};\n\n    for (let type in pos) {\n      same[type] = {};\n\n      for (let key in pos[type]) {\n        let found = false;\n\n        for (let i = 0; i < this.history.length; ++i) {\n          const point = this.history[i];\n          if (\n            !isUndefined(point[type]) &&\n            !within(point[type][key], pos[type][key])\n          ) {\n            found = true;\n            break;\n          }\n        }\n\n        if (!found) {\n          same[type][key] = true;\n        }\n      }\n    }\n\n    let css = { top: '', left: '', right: '', bottom: '' };\n\n    const transcribe = (_same, _pos) => {\n      const hasOptimizations = !isUndefined(this.options.optimizations);\n      const gpu = hasOptimizations ? this.options.optimizations.gpu : null;\n      if (gpu !== false) {\n        let yPos, xPos;\n        if (_same.top) {\n          css.top = 0;\n          yPos = _pos.top;\n        } else {\n          css.bottom = 0;\n          yPos = -_pos.bottom;\n        }\n\n        if (_same.left) {\n          css.left = 0;\n          xPos = _pos.left;\n        } else {\n          css.right = 0;\n          xPos = -_pos.right;\n        }\n\n        if (isNumber(window.devicePixelRatio) && devicePixelRatio % 1 === 0) {\n          xPos = Math.round(xPos * devicePixelRatio) / devicePixelRatio;\n          yPos = Math.round(yPos * devicePixelRatio) / devicePixelRatio;\n        }\n\n        css[transformKey] = `translateX(${xPos}px) translateY(${yPos}px)`;\n\n        if (transformKey !== 'msTransform') {\n          // The Z transform will keep this in the GPU (faster, and prevents artifacts),\n          // but IE9 doesn't support 3d transforms and will choke.\n          css[transformKey] += ' translateZ(0)';\n        }\n      } else {\n        if (_same.top) {\n          css.top = `${_pos.top}px`;\n        } else {\n          css.bottom = `${_pos.bottom}px`;\n        }\n\n        if (_same.left) {\n          css.left = `${_pos.left}px`;\n        } else {\n          css.right = `${_pos.right}px`;\n        }\n      }\n    };\n\n    const hasOptimizations = !isUndefined(this.options.optimizations);\n    let allowPositionFixed = true;\n\n    if (\n      hasOptimizations &&\n      this.options.optimizations.allowPositionFixed === false\n    ) {\n      allowPositionFixed = false;\n    }\n\n    let moved = false;\n    if (\n      (same.page.top || same.page.bottom) &&\n      (same.page.left || same.page.right)\n    ) {\n      css.position = 'absolute';\n      transcribe(same.page, pos.page);\n    } else if (\n      allowPositionFixed &&\n      (same.viewport.top || same.viewport.bottom) &&\n      (same.viewport.left || same.viewport.right)\n    ) {\n      css.position = 'fixed';\n      transcribe(same.viewport, pos.viewport);\n    } else if (\n      !isUndefined(same.offset) &&\n      same.offset.top &&\n      same.offset.left\n    ) {\n      css.position = 'absolute';\n      const offsetParent = this.cache('target-offsetparent', () =>\n        getOffsetParent(this.target)\n      );\n\n      if (getOffsetParent(this.element) !== offsetParent) {\n        defer(() => {\n          this.element?.parentNode?.removeChild(this.element);\n          offsetParent.appendChild(this.element);\n        });\n      }\n\n      transcribe(same.offset, pos.offset);\n      moved = true;\n    } else {\n      css.position = 'absolute';\n      transcribe({ top: true, left: true }, pos.page);\n    }\n\n    if (!moved) {\n      if (this.options.bodyElement) {\n        if (this.element.parentNode !== this.options.bodyElement) {\n          this.options.bodyElement.appendChild(this.element);\n        }\n      } else {\n        let offsetParentIsBody = true;\n\n        let currentNode = this.element.parentNode;\n        while (\n          currentNode &&\n          currentNode.nodeType === 1 &&\n          currentNode.tagName !== 'BODY' &&\n          !isFullscreenElement(currentNode)\n        ) {\n          if (getComputedStyle(currentNode).position !== 'static') {\n            offsetParentIsBody = false;\n            break;\n          }\n\n          currentNode = currentNode.parentNode;\n        }\n\n        if (!offsetParentIsBody) {\n          this.element?.parentNode?.removeChild(this.element);\n          this.element.ownerDocument.body.appendChild(this.element);\n        }\n      }\n    }\n\n    // Any css change will trigger a repaint, so let's avoid one if nothing changed\n    const writeCSS = {};\n    let write = false;\n    for (let key in css) {\n      let val = css[key];\n      let elVal = this.element.style[key];\n\n      if (elVal !== val) {\n        write = true;\n        writeCSS[key] = val;\n      }\n    }\n\n    if (write) {\n      defer(() => {\n        extend(this.element.style, writeCSS);\n        this.trigger('repositioned');\n      });\n    }\n  }\n\n  _addClasses() {\n    const { classes, classPrefix } = this.options;\n    addClass(this.element, getClass('element', classes, classPrefix));\n    if (!(this.options.addTargetClasses === false)) {\n      addClass(this.target, getClass('target', classes, classPrefix));\n    }\n  }\n\n  _removeClasses() {\n    const { classes, classPrefix } = this.options;\n    removeClass(this.element, getClass('element', classes, classPrefix));\n    if (!(this.options.addTargetClasses === false)) {\n      removeClass(this.target, getClass('target', classes, classPrefix));\n    }\n\n    this.all.forEach((className) => {\n      this.element.classList.remove(className);\n      this.target.classList.remove(className);\n    });\n  }\n}\n\nTetherClass.modules = [];\n\nTetherBase.position = position;\n\nlet Tether = extend(TetherClass, TetherBase);\n\nTether.modules.push({\n  initialize() {\n    const { classes, classPrefix } = this.options;\n    this.markers = {};\n\n    ['target', 'element'].forEach((type) => {\n      const el = document.createElement('div');\n      el.className = getClass(`${type}-marker`, classes, classPrefix);\n\n      const dot = document.createElement('div');\n      dot.className = getClass('marker-dot', classes, classPrefix);\n      el.appendChild(dot);\n\n      this[type].appendChild(el);\n\n      this.markers[type] = { dot, el };\n    });\n  },\n\n  destroy() {\n    ['target', 'element'].forEach((type) => {\n      if (!this.markers || !this.markers[type]) {\n        return;\n      }\n      const el = this.markers[type].el;\n      try {\n        el?.parentNode?.removeChild(el);\n      } catch (e) {\n        // Marker elements may have already been removed if the parent was removed from DOM\n        // This is expected behavior in dynamic applications\n      }\n    });\n  },\n\n  position({ manualOffset, manualTargetOffset }) {\n    const offsets = {\n      element: manualOffset,\n      target: manualTargetOffset\n    };\n\n    for (let type in offsets) {\n      const offset = offsets[type];\n      for (let side in offset) {\n        let val = offset[side];\n        if (\n          !isString(val) ||\n          (val.indexOf('%') === -1 && val.indexOf('px') === -1)\n        ) {\n          val += 'px';\n        }\n\n        if (this.markers[type] && this.markers[type].dot?.style[side] !== val) {\n          this.markers[type].dot.style[side] = val;\n        }\n      }\n    }\n\n    return true;\n  }\n});\n\nexport default Tether;\n"
  },
  {
    "path": "src/js/utils/bounds.js",
    "content": "import { defer } from './deferred';\nimport { extend, uniqueId } from './general';\nimport { isUndefined } from './type-check';\n\nconst zeroPosCache = {};\nlet zeroElement = null;\n\nexport function getBounds(body, el) {\n  let doc;\n  if (el === document) {\n    doc = document;\n    el = document.documentElement;\n  } else {\n    doc = el.ownerDocument;\n  }\n\n  const docEl = doc.documentElement;\n\n  const box = _getActualBoundingClientRect(el);\n\n  const origin = _getOrigin(body);\n\n  box.top -= origin.top;\n  box.left -= origin.left;\n\n  if (isUndefined(box.width)) {\n    box.width = document.body.scrollWidth - box.left - box.right;\n  }\n  if (isUndefined(box.height)) {\n    box.height = document.body.scrollHeight - box.top - box.bottom;\n  }\n\n  box.top = box.top - docEl.clientTop;\n  box.left = box.left - docEl.clientLeft;\n  box.right = doc.body.clientWidth - box.width - box.left;\n  box.bottom = doc.body.clientHeight - box.height - box.top;\n\n  return box;\n}\n\n/**\n * Gets bounds for when target modifiier is 'scroll-handle'\n * @param target\n * @return {{left: number, width: number, height: number}}\n */\nexport function getScrollHandleBounds(body, target) {\n  let bounds;\n  // We have to do the check for the scrollTop and if target === document.body here and set to variables\n  // because we may reset target below.\n  const targetScrollTop = target.scrollTop;\n  const targetIsBody = target === document.body;\n\n  if (targetIsBody) {\n    target = document.documentElement;\n\n    bounds = {\n      left: pageXOffset,\n      top: pageYOffset,\n      height: innerHeight,\n      width: innerWidth\n    };\n  } else {\n    bounds = getBounds(body, target);\n  }\n\n  const style = getComputedStyle(target);\n\n  const hasBottomScroll = (\n    target.scrollWidth > target.clientWidth ||\n    [style.overflow, style.overflowX].indexOf('scroll') >= 0 ||\n    !targetIsBody\n  );\n\n  let scrollBottom = 0;\n  if (hasBottomScroll) {\n    scrollBottom = 15;\n  }\n\n  const height = bounds.height - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth) - scrollBottom;\n\n  const out = {\n    width: 15,\n    height: height * 0.975 * (height / target.scrollHeight),\n    left: bounds.left + bounds.width - parseFloat(style.borderLeftWidth) - 15\n  };\n\n  let fitAdj = 0;\n  if (height < 408 && targetIsBody) {\n    fitAdj = -0.00011 * Math.pow(height, 2) - 0.00727 * height + 22.58;\n  }\n\n  if (!targetIsBody) {\n    out.height = Math.max(out.height, 24);\n  }\n\n  const scrollPercentage = targetScrollTop / (target.scrollHeight - height);\n  out.top = scrollPercentage * (height - out.height - fitAdj) + bounds.top + parseFloat(style.borderTopWidth);\n\n  if (targetIsBody) {\n    out.height = Math.max(out.height, 24);\n  }\n\n  return out;\n}\n\n/**\n * Gets bounds for when target modifiier is 'visible\n * @param target\n * @return {{top: *, left: *, width: *, height: *}}\n */\nexport function getVisibleBounds(body, target) {\n  if (target === document.body) {\n    return { top: pageYOffset, left: pageXOffset, height: innerHeight, width: innerWidth };\n  } else {\n    const bounds = getBounds(body, target);\n\n    const out = {\n      height: bounds.height,\n      width: bounds.width,\n      top: bounds.top,\n      left: bounds.left\n    };\n\n    out.height = Math.min(out.height, bounds.height - (pageYOffset - bounds.top));\n    out.height = Math.min(out.height, bounds.height - ((bounds.top + bounds.height) - (pageYOffset + innerHeight)));\n    out.height = Math.min(innerHeight, out.height);\n    out.height -= 2;\n\n    out.width = Math.min(out.width, bounds.width - (pageXOffset - bounds.left));\n    out.width = Math.min(out.width, bounds.width - ((bounds.left + bounds.width) - (pageXOffset + innerWidth)));\n    out.width = Math.min(innerWidth, out.width);\n    out.width -= 2;\n\n    if (out.top < pageYOffset) {\n      out.top = pageYOffset;\n    }\n    if (out.left < pageXOffset) {\n      out.left = pageXOffset;\n    }\n\n    return out;\n  }\n}\n\nexport function removeUtilElements(body) {\n  if (zeroElement && zeroElement?.parentNode === body) {\n    body.removeChild(zeroElement);\n  }\n  zeroElement = null;\n}\n\n/**\n * Same as native getBoundingClientRect, except it takes into account parent <frame> offsets\n * if the element lies within a nested document (<frame> or <iframe>-like).\n * @param node\n */\nfunction _getActualBoundingClientRect(node) {\n  let boundingRect = node.getBoundingClientRect();\n\n  // The original object returned by getBoundingClientRect is immutable, so we clone it\n  // We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9\n  let rect = {};\n  for (let k in boundingRect) {\n    rect[k] = boundingRect[k];\n  }\n\n  try {\n    if (node.ownerDocument !== document) {\n      let { frameElement } = node.ownerDocument.defaultView;\n      if (frameElement) {\n        let frameRect = _getActualBoundingClientRect(frameElement);\n        rect.top += frameRect.top;\n        rect.bottom += frameRect.top;\n        rect.left += frameRect.left;\n        rect.right += frameRect.left;\n      }\n    }\n  } catch (err) {\n    // Ignore \"Access is denied\" in IE11/Edge\n  }\n\n  return rect;\n}\n\nfunction _getOrigin(body) {\n  // getBoundingClientRect is unfortunately too accurate.  It introduces a pixel or two of\n  // jitter as the user scrolls that messes with our ability to detect if two positions\n  // are equivilant or not.  We place an element at the top left of the page that will\n  // get the same jitter, so we can cancel the two out.\n  let node = zeroElement;\n  if (!node || !body.contains(node)) {\n    node = document.createElement('div');\n    node.setAttribute('data-tether-id', uniqueId());\n    extend(node.style, {\n      top: 0,\n      left: 0,\n      position: 'absolute'\n    });\n\n    body.appendChild(node);\n\n    zeroElement = node;\n  }\n\n  const id = node.getAttribute('data-tether-id');\n  if (isUndefined(zeroPosCache[id])) {\n    zeroPosCache[id] = _getActualBoundingClientRect(node);\n\n    // Clear the cache when this position call is done\n    defer(() => {\n      delete zeroPosCache[id];\n    });\n  }\n\n  return zeroPosCache[id];\n}\n"
  },
  {
    "path": "src/js/utils/classes.js",
    "content": "import { isUndefined } from './type-check';\n\nexport function addClass(el, name) {\n  name.split(' ').forEach((cls) => {\n    if (cls.trim()) {\n      el.classList.add(cls);\n    }\n  });\n}\n\n/**\n * Get class string based on previously determined classes\n * @param  {String} [key=''] - default value for the classes object\n * @param  {Object} classes\n * @param  {String} classPrefix\n */\nexport function getClass(key = '', classes, classPrefix) {\n  if (!isUndefined(classes) && !isUndefined(classes[key])) {\n    if (classes[key] === false) {\n      return '';\n    }\n    return classes[key];\n  } else if (classPrefix) {\n    return `${classPrefix}-${key}`;\n  } else {\n    return key;\n  }\n}\n\nexport function removeClass(el, name) {\n  name.split(' ').forEach((cls) => {\n    if (cls.trim()) {\n      el.classList.remove(cls);\n    }\n  });\n}\n\nexport function updateClasses(el, add, all) {\n  // Of the set of 'all' classes, we need the 'add' classes, and only the\n  // 'add' classes to be set.\n  all.forEach((cls) => {\n    if (add.indexOf(cls) === -1 && el.classList.contains(cls)) {\n      removeClass(el, cls);\n    }\n  });\n\n  add.forEach((cls) => {\n    if (!el.classList.contains(cls)) {\n      addClass(el, cls);\n    }\n  });\n}\n"
  },
  {
    "path": "src/js/utils/deferred.js",
    "content": "const deferred = [];\n\nexport function defer(fn) {\n  deferred.push(fn);\n}\n\nexport function flush() {\n  let fn;\n  // eslint-disable-next-line\n  while (fn = deferred.pop()) {\n    fn();\n  }\n}\n"
  },
  {
    "path": "src/js/utils/general.js",
    "content": "let _scrollBarSize = null;\n\nexport function extend(out = {}) {\n  const args = [];\n\n  Array.prototype.push.apply(args, arguments);\n\n  args.slice(1).forEach((obj) => {\n    if (obj) {\n      for (let key in obj) {\n        if ({}.hasOwnProperty.call(obj, key)) {\n          out[key] = obj[key];\n        }\n      }\n    }\n  });\n\n  return out;\n}\n\nexport function getScrollBarSize() {\n  if (_scrollBarSize) {\n    return _scrollBarSize;\n  }\n  const inner = document.createElement('div');\n  inner.style.width = '100%';\n  inner.style.height = '200px';\n\n  const outer = document.createElement('div');\n  extend(outer.style, {\n    position: 'absolute',\n    top: 0,\n    left: 0,\n    pointerEvents: 'none',\n    visibility: 'hidden',\n    width: '200px',\n    height: '150px',\n    overflow: 'hidden'\n  });\n\n  outer.appendChild(inner);\n\n  document.body.appendChild(outer);\n\n  const widthContained = inner.offsetWidth;\n  outer.style.overflow = 'scroll';\n  let widthScroll = inner.offsetWidth;\n\n  if (widthContained === widthScroll) {\n    widthScroll = outer.clientWidth;\n  }\n\n  document.body.removeChild(outer);\n\n  const width = widthContained - widthScroll;\n\n  _scrollBarSize = { width, height: width };\n  return _scrollBarSize;\n}\n\nexport const uniqueId = (() => {\n  let id = 0;\n  return () => ++id;\n})();\n"
  },
  {
    "path": "src/js/utils/offset.js",
    "content": "import { isString, isUndefined } from './type-check';\n\nconst MIRROR_LR = {\n  center: 'center',\n  left: 'right',\n  right: 'left'\n};\n\nconst MIRROR_TB = {\n  middle: 'middle',\n  top: 'bottom',\n  bottom: 'top'\n};\n\nconst OFFSET_MAP = {\n  top: 0,\n  left: 0,\n  middle: '50%',\n  center: '50%',\n  bottom: '100%',\n  right: '100%'\n};\n\nexport function addOffset(...offsets) {\n  const out = { top: 0, left: 0 };\n\n  offsets.forEach(({ top, left }) => {\n    if (isString(top)) {\n      top = parseFloat(top);\n    }\n    if (isString(left)) {\n      left = parseFloat(left);\n    }\n\n    out.top += top;\n    out.left += left;\n  });\n\n  return out;\n}\n\nexport function attachmentToOffset(attachment) {\n  let { left, top } = attachment;\n\n  if (!isUndefined(OFFSET_MAP[attachment.left])) {\n    left = OFFSET_MAP[attachment.left];\n  }\n\n  if (!isUndefined(OFFSET_MAP[attachment.top])) {\n    top = OFFSET_MAP[attachment.top];\n  }\n\n  return { left, top };\n}\n\nexport function autoToFixedAttachment(attachment, relativeToAttachment) {\n  let { left, top } = attachment;\n\n  if (left === 'auto') {\n    left = MIRROR_LR[relativeToAttachment.left];\n  }\n\n  if (top === 'auto') {\n    top = MIRROR_TB[relativeToAttachment.top];\n  }\n\n  return { left, top };\n}\n\nexport function offsetToPx(offset, size) {\n  if (isString(offset.left) && offset.left.indexOf('%') !== -1) {\n    offset.left = parseFloat(offset.left) / 100 * size.width;\n  }\n  if (isString(offset.top) && offset.top.indexOf('%') !== -1) {\n    offset.top = parseFloat(offset.top) / 100 * size.height;\n  }\n\n  return offset;\n}\n\nexport function parseTopLeft(value) {\n  const [top, left] = value.split(' ');\n  return { top, left };\n}\n"
  },
  {
    "path": "src/js/utils/parents.js",
    "content": "import { isUndefined } from './type-check';\n\nexport function getScrollParents(el) {\n  // In firefox if the el is inside an iframe with display: none; window.getComputedStyle() will return null;\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=548397\n  const computedStyle = getComputedStyle(el) || {};\n  const { position } = computedStyle;\n  let parents = [];\n\n  if (position === 'fixed') {\n    return [el];\n  }\n\n  let parent = el;\n  while ((parent = parent.parentNode) && parent && parent.nodeType === 1) {\n    let style;\n    try {\n      style = getComputedStyle(parent);\n    } catch (err) {\n      // Intentionally blank\n    }\n\n    if (isUndefined(style) || style === null) {\n      parents.push(parent);\n      return parents;\n    }\n\n    const { overflow, overflowX, overflowY } = style;\n    if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {\n      if (position !== 'absolute' || ['relative', 'absolute', 'fixed'].indexOf(style.position) >= 0) {\n        parents.push(parent);\n      }\n    }\n  }\n\n  parents.push(el.ownerDocument.body);\n\n  // If the node is within a frame, account for the parent window scroll\n  if (el.ownerDocument !== document) {\n    parents.push(el.ownerDocument.defaultView);\n  }\n\n  return parents;\n}\n\nexport function getOffsetParent(el) {\n  return el.offsetParent || document.documentElement;\n}\n"
  },
  {
    "path": "src/js/utils/type-check.js",
    "content": "/**\n * Checks if `value` is classified as a `Function` object.\n * @param {*} value The param to check if it is a function\n */\nexport function isFunction(value) {\n  return typeof value === 'function';\n}\n\n/**\n * Checks if `value` is classified as a `Number` object.\n * @param {*} value The param to check if it is a number\n */\nexport function isNumber(value) {\n  return typeof value === 'number';\n}\n\n/**\n * Checks if `value` is classified as an `Object`.\n * @param {*} value The param to check if it is an object\n */\nexport function isObject(value) {\n  return typeof value === 'object';\n}\n\n/**\n * Checks if `value` is classified as a `String` object.\n * @param {*} value The param to check if it is a string\n */\nexport function isString(value) {\n  return typeof value === 'string';\n}\n\n/**\n * Checks if `value` is undefined.\n * @param {*} value The param to check if it is undefined\n */\nexport function isUndefined(value) {\n  return value === undefined;\n}\n"
  },
  {
    "path": "test/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  parserOptions: {\n    ecmaVersion: 2019,\n    sourceType: 'module'\n  },\n  plugins: [\n    'jest'\n  ],\n  extends: [\n    'eslint:recommended',\n    'plugin:jest/recommended'\n  ],\n  globals: {\n    Cypress: false,\n    Event: true,\n    MouseEvent: true,\n    Shepherd: false,\n    assert: false,\n    cy: false,\n    document: false,\n    expect: false,\n    require: false,\n    window: false\n  },\n  env: {\n    es6: true,\n    mocha: true,\n    node: true\n  },\n  rules: {\n    'jest/valid-expect': 'off',\n    'jest/valid-expect-in-promise': 'off',\n    'no-console': 'off'\n  }\n};\n"
  },
  {
    "path": "test/cypress/integration/content-visible.cy.js",
    "content": "describe('content-visible', () => {\n  beforeEach(() => {\n    cy.visit('/examples/content-visible/');\n    cy.scrollTo(0, 0);\n  });\n\n  describe('enable/disable works', () => {\n    it('scrolling moves the element', () => {\n      cy.get('.tether-target').should('have.class', 'tether-enabled');\n      cy.get('.tether-element').then((tetherElement) => {\n        const prescrollTransform = tetherElement[0].style.transform;\n\n        cy.scrollTo(0, 1000);\n        cy.wait(500);\n        cy.get('.tether-element').then((tetherElement) => {\n          const postscrollTransform = tetherElement[0].style.transform;\n          expect(prescrollTransform).to.not.equal(postscrollTransform);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/integration/enable-disable.cy.js",
    "content": "describe('enable-disable', () => {\n  beforeEach(() => {\n    cy.visit('/examples/enable-disable/');\n    cy.get('.container').scrollTo(0, 0);\n  });\n\n  describe('enable/disable works', () => {\n    it('enable should apply transforms', () => {\n      cy.get('.tether-target').should('have.class', 'tether-enabled');\n      cy.get('.tether-element').then((tetherElement) => {\n        const prescrollTransform = tetherElement[0].style.transform;\n\n        cy.get('.container').scrollTo(0, 250);\n        cy.wait(1000);\n        cy.get('.tether-element').then((tetherElement) => {\n          const postscrollTransform = tetherElement[0].style.transform;\n          expect(prescrollTransform).to.not.equal(postscrollTransform);\n        });\n      });\n    });\n\n    it('disable should not apply transforms', () => {\n      cy.get('.tether-target').click();\n      cy.get('.tether-target').should('not.have.class', 'tether-enabled');\n      cy.get('.tether-element').then((tetherElement) => {\n        const prescrollTransform = tetherElement[0].style.transform;\n\n        cy.get('.container').scrollTo(0, 250);\n        cy.wait(500);\n        cy.get('.tether-element').then((tetherElement) => {\n          const postscrollTransform = tetherElement[0].style.transform;\n          expect(prescrollTransform).to.equal(postscrollTransform);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/integration/out-of-bounds.cy.js",
    "content": "describe('out of bounds', () => {\n  beforeEach(() => {\n    cy.visit('/examples/out-of-bounds/');\n  });\n\n  describe('tether element hidden', () => {\n    it('tether-element should hide when it cannot fit', () => {\n      cy.get('.tether-element').should('not.have.class', 'tether-out-of-bounds');\n      cy.get('.tether-element').should('not.have.class', 'tether-out-of-bounds-left');\n      cy.viewport(500, 500);\n      cy.get('.tether-element').should('have.class', 'tether-out-of-bounds');\n      cy.get('.tether-element').should('have.class', 'tether-out-of-bounds-left');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/integration/pin.cy.js",
    "content": "describe('pin', () => {\n  beforeEach(() => {\n    cy.visit('/examples/pin/');\n  });\n\n  describe('tether element pinned', () => {\n    it('tether-element should pin on scroll', () => {\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned');\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned-top');\n      cy.scrollTo(0, 300);\n      cy.get('.tether-element').should('have.class', 'tether-pinned');\n      cy.get('.tether-element').should('have.class', 'tether-pinned-top');\n      cy.scrollTo(0, 0);\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned');\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned-top');\n    });\n\n    it('tether-element should pin on resize', () => {\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned');\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned-right');\n      cy.viewport(700, 700);\n      cy.get('.tether-element').should('have.class', 'tether-pinned');\n      cy.get('.tether-element').should('have.class', 'tether-pinned-right');\n      cy.viewport(1500, 700);\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned');\n      cy.get('.tether-element').should('not.have.class', 'tether-pinned-right');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/integration/scroll.cy.js",
    "content": "describe('Scroll', () => {\n  beforeEach(() => {\n    cy.visit('/examples/scroll/', {});\n  });\n\n  describe('tether stays near scrollbar', () => {\n    it('appears on scroll and stays in fixed position', () => {\n      let tetherFixed;\n      let tetherOffsetTop;\n\n      cy.get('.tether-target').and(el => {\n        // set the initial position coords\n        tetherFixed = el.find('.pointer');\n        // starting point is 9px from top of document\n        tetherOffsetTop = tetherFixed[0].getBoundingClientRect().top - 9;\n\n        expect(el).to.contain('.pointer');\n      });\n      cy.get('.pointer').should('have.css', 'opacity', '0');\n\n      cy.scrollTo(0, 1500);\n\n      cy.get('.pointer').should('have.css', 'opacity', '1');\n      cy.get('.pointer').and(el => {\n        const vpOffset = el[0].getBoundingClientRect();\n        // we measure from top minus scrolled coords\n        // Allow for sub-pixel rounding differences\n        expect(vpOffset.top + window.pageYOffset).to.be.closeTo(tetherOffsetTop, 1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/integration/simple.cy.js",
    "content": "describe('simple', () => {\n  // let Tether;\n\n  beforeEach(() => {\n    // Tether = null;\n\n    cy.visit('/examples/simple/', {\n      // onLoad(contentWindow) {\n      //   if (contentWindow.Tether) {\n      //     return Tether = contentWindow.Tether;\n      //   }\n      // }\n    });\n  });\n\n  describe('flip works', () => {\n    it('tether-element should flip on resize', () => {\n      cy.get('.tether-element').should('have.class', 'tether-target-attached-right');\n      cy.get('.tether-element').should('not.have.class', 'tether-target-attached-left');\n      cy.viewport(700, 700);\n      cy.get('.tether-element').should('have.class', 'tether-target-attached-left');\n      cy.get('.tether-element').should('not.have.class', 'tether-target-attached-right');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/integration/testbed.cy.js",
    "content": "describe('testbed', () => {\n  beforeEach(() => {\n    cy.visit('/examples/testbed/');\n  });\n\n  describe('flip works', () => {\n    it('tether-element should flip on scroll', () => {\n      cy.get('.tether-element').should('have.class', 'tether-target-attached-top');\n      cy.get('.tether-element').should('not.have.class', 'tether-target-attached-bottom');\n      cy.get('.container').scrollTo(0, 250);\n      cy.get('.tether-element').should('have.class', 'tether-target-attached-bottom');\n      cy.get('.tether-element').should('not.have.class', 'tether-target-attached-top');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/plugins/index.js",
    "content": "// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nmodule.exports = (/* on, config */) => {\n};\n"
  },
  {
    "path": "test/cypress/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add(\"login\", (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add(\"drag\", { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add(\"dismiss\", { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This is will overwrite an existing command --\n// Cypress.Commands.overwrite(\"visit\", (originalFn, url, options) => { ... })\n"
  },
  {
    "path": "test/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands';\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "test/unit/constraint.spec.js",
    "content": "import Constraint from '../../src/js/constraint.js';\n\ndescribe('Constraint', () => {\n  describe('getBoundingRect()', () => {\n    const getBoundingRect = Constraint.__get__('getBoundingRect');\n    let element;\n\n    beforeEach(() => {\n      element = document.createElement('div');\n      element.classList.add('element');\n      document.body.appendChild(element);\n    });\n\n    afterEach(() => {\n      document.body.removeChild(element);\n      element = null;\n    });\n\n    it('returns null with no args', () => {\n      expect(getBoundingRect()).toBeNull();\n    });\n\n    it('return bounds from border width when constraint is scrollParent', () => {\n      element.style.borderWidth = '4px';\n      const scrollParentBounds = getBoundingRect(document.body, { scrollParents: [element] }, 'scrollParent');\n\n      expect(scrollParentBounds).toHaveLength(4);\n      expect(scrollParentBounds).toEqual(expect.arrayContaining([4, 4, -4, -4]));\n    });\n\n    it('return bounds from current window when constraint is window', () => {\n      const windowBounds = getBoundingRect(document.body, { scrollParents: [element] }, 'window');\n\n      expect(windowBounds).toHaveLength(4);\n      expect(windowBounds).toEqual(expect.arrayContaining([0, 0, 1024, 768]));\n    });\n\n    it('return bounds from document window when constraint is document', () => {\n      document.documentElement.style.borderWidth = '0';\n      const windowBounds = getBoundingRect(document.body, { scrollParents: [] }, document);\n\n      expect(windowBounds).toHaveLength(4);\n      expect(windowBounds).toEqual(expect.arrayContaining([0, 0, 0, 0]));\n    });\n  });\n\n  describe('_addOutOfBoundsClass()', () => {\n    const _addOutOfBoundsClass = Constraint.__get__('_addOutOfBoundsClass');\n    let oob;\n\n    beforeEach(() => {\n      oob = [];\n    });\n\n\n    it('adds nothing if out of bounds array is empty', () => {\n      _addOutOfBoundsClass(oob, [], [], '', '');\n\n      expect(oob).toHaveLength(0);\n    });\n\n    it('does not add a class if oob class option is false', () => {\n      oob.push('top');\n      const addClasses = [];\n      _addOutOfBoundsClass(oob, addClasses, {\n        'out-of-bounds': false\n      });\n\n      expect(addClasses).toHaveLength(2);\n      expect(addClasses).toEqual(expect.arrayContaining(['', '-top']));\n    });\n    it('adds classes for oob prefix and options classes', () => {\n      oob.push('top');\n      const addClasses = [];\n      _addOutOfBoundsClass(oob, addClasses, {\n        'out-of-bounds': 'added'\n      });\n\n      expect(addClasses).toHaveLength(2);\n      expect(addClasses).toEqual(expect.arrayContaining(['added', 'added-top']));\n    });\n\n    it('uses extra prefix for outOfBoundsClass', () => {\n      oob.push('top');\n      const addClasses = [];\n      _addOutOfBoundsClass(oob, addClasses, {\n        'out-of-bounds': 'added'\n      }, '', 'extra');\n\n      expect(addClasses).toHaveLength(2);\n      expect(addClasses).toEqual(expect.arrayContaining(['extra', 'extra-top']));\n    });\n  });\n\n  describe('_calculateOOBAndPinnedLeft', () => {\n    const _calculateOOBAndPinnedLeft = Constraint.__get__('_calculateOOBAndPinnedLeft');\n    const bounds = [10, 10, 20, 20];\n    let left, oob, pinned;\n\n    beforeEach(() => {\n      left = 0;\n      oob = [];\n      pinned = [];\n    });\n\n    it('left < leftBound', () => {\n      const pin = [];\n      const width = 10;\n\n      left = _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob);\n      expect(pinned.includes('left'), 'pinned does not include \"left\"').toBe(false);\n      expect(oob.includes('left'), 'oob includes \"left\"').toBe(true);\n      expect(left, 'left remains the same').toEqual(0);\n    });\n\n    it('left < leftBound: pin left', () => {\n      const pin = ['left'];\n      const width = 10;\n\n      left = _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob);\n      expect(pinned.includes('left'), 'pinned includes \"left\"').toBe(true);\n      expect(oob.includes('left'), 'oob does not include \"left\"').toBe(false);\n      expect(left, 'left set to leftBound').toEqual(10);\n    });\n\n    it('left + width > rightBound', () => {\n      const pin = [];\n      const width = 100;\n\n      left = _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob);\n      expect(pinned.includes('right'), 'pinned does not include \"right\"').toBe(false);\n      expect(oob.includes('right'), 'oob includes \"right\"').toBe(true);\n      expect(left, 'left remains the same').toEqual(0);\n    });\n\n    it('left + width > rightBound: pin right', () => {\n      const pin = ['right'];\n      const width = 100;\n\n      left = _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob);\n      expect(pinned.includes('right'), 'pinned includes \"right\"').toBe(true);\n      expect(oob.includes('right'), 'oob does not include \"right\"').toBe(false);\n      expect(left, 'left set to rightBound - width').toEqual(-80);\n    });\n  });\n\n  describe('_calculateOOBAndPinnedTop', () => {\n    const _calculateOOBAndPinnedTop = Constraint.__get__('_calculateOOBAndPinnedTop');\n    const bounds = [10, 10, 20, 20];\n    let oob, pinned, top;\n\n    beforeEach(() => {\n      top = 0;\n      oob = [];\n      pinned = [];\n    });\n\n    it('top < topBound', () => {\n      const pin = [];\n      const height = 10;\n\n      top = _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob);\n      expect(pinned.includes('top'), 'pinned does not include \"top\"').toBe(false);\n      expect(oob.includes('top'), 'oob includes \"top\"').toBe(true);\n      expect(top, 'top remains the same').toEqual(0);\n    });\n\n    it('top < topBound: pin top', () => {\n      const pin = ['top'];\n      const height = 10;\n\n      top = _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob);\n      expect(pinned.includes('top'), 'pinned includes \"top\"').toBe(true);\n      expect(oob.includes('top'), 'oob does not include \"top\"').toBe(false);\n      expect(top, 'top set to topBound').toEqual(10);\n    });\n\n    it('top + height > bottomBound', () => {\n      const pin = [];\n      const height = 100;\n\n      top = _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob);\n      expect(pinned.includes('bottom'), 'pinned does not include \"bottom\"').toBe(false);\n      expect(oob.includes('bottom'), 'oob includes \"bottom\"').toBe(true);\n      expect(top, 'top remains the same').toEqual(0);\n    });\n\n    it('top + height > bottomBound: pin bottom', () => {\n      const pin = ['bottom'];\n      const height = 100;\n\n      top = _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob);\n      expect(pinned.includes('bottom'), 'pinned includes \"bottom\"').toBe(true);\n      expect(oob.includes('bottom'), 'oob does not \"bottom\"').toBe(false);\n      expect(top, 'top set to bottomBound - height').toEqual(-80);\n    });\n  });\n\n  describe('_flipXTogether', () => {\n    const _flipXTogether = Constraint.__get__('_flipXTogether');\n    let bounds, eAttachment, left, targetWidth, tAttachment, width;\n\n    describe('left < leftBounds && tAttachment.left === \"left\"', () => {\n      beforeEach(() => {\n        bounds = [10, 10, 20, 20];\n        left = 0;\n        tAttachment = { left: 'left' };\n        targetWidth = 10;\n        width = 7;\n      });\n\n      it('eAttachment.left === \"right\"', () => {\n        eAttachment = { left: 'right' };\n        left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left);\n        expect(left, 'targetWidth and width added to left').toEqual(17);\n        expect(tAttachment.left, 'target attachment flipped to right').toBe('right');\n        expect(eAttachment.left, 'element attachment flipped to left').toBe('left');\n      });\n\n      it('eAttachment.left === \"left\"', () => {\n        eAttachment = { left: 'left' };\n        left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left);\n        expect(left, 'targetWidth added and width subtracted from left').toEqual(3);\n        expect(tAttachment.left, 'target attachment flipped to right').toBe('right');\n        expect(eAttachment.left, 'element attachment flipped to right').toBe('right');\n      });\n    });\n\n    describe('left + width > rightBounds && tAttachment.left === \"right\"', () => {\n      beforeEach(() => {\n        bounds = [10, 10, 20, 20];\n        left = 0;\n        tAttachment = { left: 'right' };\n        targetWidth = 100;\n        width = 50;\n      });\n\n      it('eAttachment.left === \"left\"', () => {\n        eAttachment = { left: 'left' };\n        left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left);\n        expect(left, 'targetWidth and width subtracted from left').toEqual(-150);\n        expect(tAttachment.left, 'target attachment flipped to left').toBe('left');\n        expect(eAttachment.left, 'element attachment flipped to right').toBe('right');\n      });\n\n      it('eAttachment.left === \"right\"', () => {\n        eAttachment = { left: 'right' };\n        left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left);\n        expect(left, 'targetWidth subtracted and width added to left').toEqual(-50);\n        expect(tAttachment.left, 'target attachment flipped to left').toBe('left');\n        expect(eAttachment.left, 'element attachment flipped to left').toBe('left');\n      });\n    });\n\n    describe('tAttachment.left === \"center\"', () => {\n      beforeEach(() => {\n        bounds = [10, 10, 20, 20];\n        left = 0;\n        tAttachment = { left: 'center' };\n        targetWidth = 100;\n        width = 50;\n      });\n\n      it('left + width > rightBounds && eAttachment.left === \"left\"', () => {\n        eAttachment = { left: 'left' };\n        left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left);\n        expect(left, 'width subtracted from left').toEqual(-50);\n        expect(tAttachment.left, 'target attachment kept as center').toBe('center');\n        expect(eAttachment.left, 'element attachment flipped to right').toBe('right');\n      });\n\n      it('left < leftBounds && eAttachment.left === \"right\"', () => {\n        eAttachment = { left: 'right' };\n        left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left);\n        expect(left, 'width added to left').toEqual(50);\n        expect(tAttachment.left, 'target attachment kept as center').toBe('center');\n        expect(eAttachment.left, 'element attachment flipped to left').toBe('left');\n      });\n    });\n  });\n\n  describe('_flipYTogether', () => {\n    const _flipYTogether = Constraint.__get__('_flipYTogether');\n    let eAttachment, tAttachment;\n\n    describe('tAttachment.top === \"top\"', () => {\n      beforeEach(() => {\n        tAttachment = { top: 'top' };\n      });\n\n      it('eAttachment.top === \"bottom\" && top < topBounds', () => {\n        eAttachment = { top: 'bottom' };\n        const bounds = [10, 10, 75, 75];\n        const height = 10;\n        const targetHeight = 50;\n        let top = 0;\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n        expect(top, 'targetHeight added to top').toEqual(60);\n        expect(tAttachment.top, 'target attachment flipped to bottom').toBe('bottom');\n        expect(eAttachment.top, 'element attachment flipped to top').toBe('top');\n      });\n\n      //TODO figure out better naming for these cases\n      it('eAttachment.top === \"bottom\" && top < topBounds: and then hits second if too', () => {\n        eAttachment = { top: 'bottom' };\n        const bounds = [10, 10, 20, 20];\n        const height = 10;\n        const targetHeight = 50;\n        let top = 0;\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n        expect(top, 'targetHeight added to top').toEqual(0);\n        expect(tAttachment.top, 'target attachment kept as top').toBe('top');\n        expect(eAttachment.top, 'element attachment kept as bottom').toBe('bottom');\n      });\n\n      it('eAttachment.top === \"top\" && top + height > bottomBounds && top - (height - targetHeight) >= topBounds', () => {\n        eAttachment = { top: 'top' };\n        const bounds = [10, 10, 200, 200];\n        const height = 175;\n        const targetHeight = 100;\n        let top = 100;\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n        expect(top, 'top -= height - targetHeight').toEqual(25);\n        expect(tAttachment.top, 'target attachment flipped to bottom').toBe('bottom');\n        expect(eAttachment.top, 'element attachment kept as bottom').toBe('bottom');\n      });\n    });\n\n    describe('tAttachment.top === \"bottom\"', () => {\n      beforeEach(() => {\n        tAttachment = { top: 'bottom' };\n      });\n\n      it('eAttachment.top === \"top\" && top + height > bottomBounds', () => {\n        eAttachment = { top: 'top' };\n        const bounds = [10, 10, 75, 75];\n        const height = 100;\n        const targetHeight = 50;\n        let top = 0;\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n        expect(top, 'targetHeight added to top').toEqual(-150);\n        expect(tAttachment.top, 'target attachment flipped to top').toBe('top');\n        expect(eAttachment.top, 'element attachment flipped to bottom').toBe('bottom');\n      });\n\n      it('eAttachment.top === \"bottom\" && top < topBounds && top + (height * 2 - targetHeight) <= bottomBounds', () => {\n        eAttachment = { top: 'bottom' };\n        const bounds = [10, 10, 200, 200];\n        const height = 100;\n        const targetHeight = 50;\n        let top = 0;\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n        expect(top, 'targetHeight added to top').toEqual(50);\n        expect(tAttachment.top, 'target attachment flipped to top').toBe('top');\n        expect(eAttachment.top, 'element attachment flipped to top').toBe('top');\n      });\n    });\n\n    describe('tAttachment.top === \"middle\"', () => {\n      let bounds, height, targetHeight, top;\n\n      beforeEach(() => {\n        bounds = [10, 10, 75, 75];\n        height = 100;\n        targetHeight = 50;\n        top = 0;\n        tAttachment = { top: 'middle' };\n      });\n\n      it('top + height > bottomBounds && eAttachment.top === \"top\"', () => {\n        eAttachment = { top: 'top' };\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n        expect(top, 'targetHeight added to top').toEqual(-100);\n        expect(tAttachment.top, 'target attachment flipped to middle').toBe('middle');\n        expect(eAttachment.top, 'element attachment kept as bottom').toBe('bottom');\n      });\n\n      it('top < topBounds && eAttachment.top === \"bottom\"', () => {\n        eAttachment = { top: 'bottom' };\n        top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top);\n        expect(top, 'targetHeight added to top').toEqual(100);\n        expect(tAttachment.top, 'target attachment flipped to middle').toBe('middle');\n        expect(eAttachment.top, 'element attachment flipped to top').toBe('top');\n      });\n    });\n  });\n\n  describe('_getAllClasses', () => {\n    const _getAllClasses = Constraint.__get__('_getAllClasses');\n\n    it('returns all the base classes when no changes passed', () => {\n      const baseClasses = _getAllClasses({}, '', []);\n\n      expect(baseClasses).toHaveLength(10);\n      expect(baseClasses).toEqual(expect.arrayContaining(['pinned',\n        'out-of-bounds',\n        'pinned-left',\n        'pinned-top',\n        'pinned-right',\n        'pinned-bottom',\n        'out-of-bounds-left',\n        'out-of-bounds-top',\n        'out-of-bounds-right',\n        'out-of-bounds-bottom']));\n    });\n\n    it('returns all the base classes with the passed prefix', () => {\n      const prefixClasses = _getAllClasses({}, 'prefix', []);\n\n      expect(prefixClasses).toHaveLength(10);\n      expect(prefixClasses).toEqual(expect.arrayContaining([\n        'prefix-pinned',\n        'prefix-out-of-bounds',\n        'prefix-pinned-left',\n        'prefix-pinned-top',\n        'prefix-pinned-right',\n        'prefix-pinned-bottom',\n        'prefix-out-of-bounds-left',\n        'prefix-out-of-bounds-top',\n        'prefix-out-of-bounds-right',\n        'prefix-out-of-bounds-bottom']));\n    });\n\n    it('replaces a class when a replacement name is passed', () => {\n      const replaceClass = _getAllClasses({\n        'pinned': 'stuck'\n      }, '', []);\n\n      expect(replaceClass).toHaveLength(10);\n      expect(replaceClass).toEqual(expect.arrayContaining([\n        'stuck',\n        'out-of-bounds',\n        'stuck-left',\n        'stuck-top',\n        'stuck-right',\n        'stuck-bottom',\n        'out-of-bounds-left',\n        'out-of-bounds-top',\n        'out-of-bounds-right',\n        'out-of-bounds-bottom']));\n    });\n\n    it('adds a constraint class and variations for sides', () => {\n      const constraintClasses = _getAllClasses({}, '', [{ outOfBoundsClass: 'constraintOob' }]);\n\n      expect(constraintClasses).toHaveLength(15);\n      expect(constraintClasses).toContain('constraintOob');\n      expect(constraintClasses).toContain('constraintOob-top');\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/evented.spec.js",
    "content": "import { Evented } from '../../src/js/evented.js';\nimport { spy } from 'sinon';\n\ndescribe('Evented', () => {\n  let testEvent, testOnTriggered;\n\n  beforeEach(() => {\n    testEvent = new Evented();\n    testEvent.on('testOn', () => testOnTriggered = true);\n    testOnTriggered = false;\n  });\n\n  describe('chaining', ()=>{\n    it('allows chaining', () => {\n      const chain = testEvent.on('foo').off('foo').trigger('foo');\n      expect(chain, 'chaining returns evented reference').toEqual(testEvent);\n    });\n  });\n\n  describe('on()', () => {\n    it('adds a new event binding', () => {\n      expect(testEvent.bindings.testOn, 'custom event added').toBeTruthy();\n    });\n  });\n\n  describe('trigger()', () => {\n    it('triggers a created event', () => {\n      testEvent.trigger('testOn');\n      expect(testOnTriggered, 'true is returned from event trigger').toBeTruthy();\n    });\n\n    it('passes arguments to handler functions', () => {\n      const handlerSpy = spy();\n      testEvent.on('myEvent', handlerSpy);\n      testEvent.trigger('myEvent', {\n        step: { id: 'test', text: 'A step' },\n        previous: null\n      });\n      expect(handlerSpy.args).toEqual([[{ 'previous': null, 'step': { 'id': 'test', 'text': 'A step' } }]]);\n    });\n  });\n\n  describe('off()', () => {\n    it('removes a generic event binding when no handler passed', () => {\n      testEvent.off('testOn');\n      expect(testEvent.bindings.testOn, 'custom event removed').toBeUndefined();\n    });\n\n    it('removes a specific event binding when handler is passed', () => {\n      const handler = () => {};\n      testEvent.on('testOn', handler);\n      expect(testEvent.bindings.testOn.length, '2 event listeners for testOn').toBe(2);\n      testEvent.off('testOn', handler);\n      expect(testEvent.bindings.testOn.length, '1 event listener for testOn').toBe(1);\n    });\n\n    it('does not remove uncreated events', () => {\n      expect(testEvent.off('testBlank'), 'returns evented reference for non created events').toEqual(testEvent);\n    });\n  });\n\n  describe('once()', () => {\n    it('adds a new event binding that only triggers once', () => {\n      testEvent.once('testOnce', () => true);\n      testEvent.trigger('testOnce');\n      expect(testEvent.bindings.testOnce, 'custom event removed after one trigger').toBeTruthy();\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/setupTests.js",
    "content": "import 'regenerator-runtime/runtime';\nimport 'jest-expect-message';\nimport '@testing-library/jest-dom/extend-expect';\nimport 'mutationobserver-shim';\n\nglobal.sleep = ms => new Promise( resolve => setTimeout(resolve, ms) );\n"
  },
  {
    "path": "test/unit/shift.spec.js",
    "content": "import shift from '../../src/js/shift';\n\ndescribe('Shift', () => {\n  describe('position()', () => {\n    it('returns undefined when shift option is not set', () => {\n      const context = {\n        options: {}\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(result).toBeUndefined();\n    });\n\n    it('shifts position with string value (single value applies to both)', () => {\n      const context = {\n        options: { shift: '10' }\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(result).toEqual({ top: 20, left: 30 });\n    });\n\n    it('shifts position with string value (two values)', () => {\n      const context = {\n        options: { shift: '10 5' }\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(result).toEqual({ top: 20, left: 25 });\n    });\n\n    it('shifts position with object value', () => {\n      const context = {\n        options: { shift: { top: 15, left: 25 } }\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(result).toEqual({ top: 25, left: 45 });\n    });\n\n    it('shifts position with function value', () => {\n      const shiftFn = jest.fn(() => ({ top: 5, left: 10 }));\n      const context = {\n        options: { shift: shiftFn }\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(shiftFn).toHaveBeenCalledWith({ top: 10, left: 20 });\n      expect(result).toEqual({ top: 15, left: 30 });\n    });\n\n    it('handles function returning string', () => {\n      const shiftFn = jest.fn(() => '20 30');\n      const context = {\n        options: { shift: shiftFn }\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(result).toEqual({ top: 30, left: 50 });\n    });\n\n    it('handles function returning object', () => {\n      const shiftFn = jest.fn(() => ({ top: 100, left: 200 }));\n      const context = {\n        options: { shift: shiftFn }\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(result).toEqual({ top: 110, left: 220 });\n    });\n\n    it('handles negative shift values', () => {\n      const context = {\n        options: { shift: '-5 -10' }\n      };\n      const result = shift.position.call(context, { top: 10, left: 20 });\n      expect(result).toEqual({ top: 5, left: 10 });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/tether.spec.js",
    "content": "import Tether from '../../src/js/tether.js';\n\ndescribe('Tether', () => {\n  let element, target;\n\n  beforeEach(() => {\n    element = document.createElement('div');\n    element.classList.add('element');\n    document.body.appendChild(element);\n    target = document.createElement('div');\n    target.classList.add('target');\n    document.body.appendChild(target);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(element);\n    document.body.removeChild(target);\n    element = null;\n    target = null;\n  });\n\n  describe('destroy()', () => {\n    it('removes classes on destroy', () => {\n      expect(element.classList.length, 'element - only one class').toEqual(1);\n      expect(target.classList.length, 'target - only one class').toEqual(1);\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right'\n      });\n\n      tether.enable();\n\n      expect(element.classList.length, 'element - tether classes added').toEqual(12);\n      expect(target.classList.length, 'target - tether classes added').toEqual(12);\n\n      tether.destroy();\n\n      expect(element.classList.length, 'element - destroy sets classes back to initial state').toEqual(1);\n      expect(target.classList.length, 'target - destroy sets classes back to initial state').toEqual(1);\n    });\n  });\n\n  describe('getClass()', () => {\n    it('gets default classes when no options set', () => {\n      expect(element.classList.length, 'element - only one class').toEqual(1);\n      expect(target.classList.length, 'target - only one class').toEqual(1);\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right'\n      });\n\n      tether.enable();\n\n      expect(element.classList.length, 'element - tether classes added').toEqual(12);\n      expect(element).toHaveClass('tether-element');\n      expect(element).not.toHaveClass('tether-target');\n\n      expect(target.classList.length, 'target - tether classes added').toEqual(12);\n      expect(target).toHaveClass('tether-target');\n      expect(target).not.toHaveClass('tether-element');\n\n      tether.destroy();\n\n      expect(element.classList.length, 'element - destroy sets classes back to initial state').toEqual(1);\n      expect(target.classList.length, 'target - destroy sets classes back to initial state').toEqual(1);\n    });\n\n    it('gets prefixed classes when classPrefix set', () => {\n      expect(element.classList.length, 'element - only one class').toEqual(1);\n      expect(target.classList.length, 'target - only one class').toEqual(1);\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right',\n        classPrefix: 'foo'\n      });\n\n      tether.enable();\n\n      expect(element.classList.length, 'element - foo classes added').toEqual(12);\n      expect(element).toHaveClass('foo-element');\n      expect(element).not.toHaveClass('foo-target');\n\n      expect(target.classList.length, 'target - foo classes added').toEqual(12);\n      expect(target).toHaveClass('foo-target');\n      expect(target).not.toHaveClass('foo-element');\n\n      tether.destroy();\n\n      expect(element.classList.length, 'element - destroy sets classes back to initial state').toEqual(1);\n      expect(target.classList.length, 'target - destroy sets classes back to initial state').toEqual(1);\n    });\n\n    it('gets overridden classes', () => {\n      expect(element.classList.length, 'element - only one class').toEqual(1);\n      expect(target.classList.length, 'target - only one class').toEqual(1);\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right',\n        classes: {\n          element: 'my-custom-class',\n          target: 'another-one'\n        }\n      });\n\n      tether.enable();\n\n      expect(element.classList.length, 'element - custom classes added').toEqual(12);\n      expect(element).toHaveClass('my-custom-class');\n      expect(element).not.toHaveClass('another-one');\n      expect(element).not.toHaveClass('tether-element');\n\n      expect(target.classList.length, 'target - custom classes added').toEqual(12);\n      expect(target).toHaveClass('another-one');\n      expect(target).not.toHaveClass('my-custom-class');\n      expect(target).not.toHaveClass('tether-target');\n\n      tether.destroy();\n\n      expect(element.classList.length, 'element - destroy sets classes back to initial state').toEqual(1);\n      expect(target.classList.length, 'target - destroy sets classes back to initial state').toEqual(1);\n    });\n\n    it('removes classes when false', () => {\n      expect(element.classList.length, 'element - only one class').toEqual(1);\n      expect(target.classList.length, 'target - only one class').toEqual(1);\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right',\n        classes: {\n          element: false,\n          enabled: false,\n          target: false\n        }\n      });\n\n      tether.enable();\n\n      expect(element.classList.length, 'element - classes added').toEqual(10);\n      expect(element).not.toHaveClass('tether-element');\n      expect(element).not.toHaveClass('tether-enabled');\n\n      expect(target.classList.length, 'target - classes added').toEqual(10);\n      expect(target).not.toHaveClass('tether-target');\n      expect(element).not.toHaveClass('tether-enabled');\n\n      tether.destroy();\n\n      expect(element.classList.length, 'element - destroy sets classes back to initial state').toEqual(1);\n      expect(target.classList.length, 'target - destroy sets classes back to initial state').toEqual(1);\n    });\n  });\n\n  describe('defensive DOM removal', () => {\n    it('should handle destroy when target is removed from DOM', () => {\n      expect.assertions(2);\n\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right'\n      });\n\n      tether.enable();\n\n      // Verify tether is enabled\n      expect(tether.enabled).toBe(true);\n\n      // Disable first to prevent any positioning attempts\n      tether.disable();\n\n      // Remove target from DOM before destroying tether\n      document.body.removeChild(target);\n\n      // This should not throw an error\n      expect(() => {\n        tether.destroy();\n      }).not.toThrow();\n\n      // Clean up element - it might have been moved by tether\n      if (element.parentNode) {\n        element.parentNode.removeChild(element);\n      }\n\n      // Put element back in body for afterEach\n      if (!element.parentNode) {\n        document.body.appendChild(element);\n      }\n\n      // Reset target to a new one for afterEach\n      target = document.createElement('div');\n      target.classList.add('target');\n      document.body.appendChild(target);\n    });\n\n    it('should handle destroy when element is removed from DOM', () => {\n      expect.assertions(2);\n\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right'\n      });\n\n      tether.enable();\n\n      // Verify tether is enabled\n      expect(tether.enabled).toBe(true);\n\n      // Disable first to prevent any positioning attempts\n      tether.disable();\n\n      // Remove element from DOM before destroying tether\n      const currentParent = element.parentNode;\n      if (currentParent) {\n        currentParent.removeChild(element);\n      }\n\n      // This should not throw an error\n      expect(() => {\n        tether.destroy();\n      }).not.toThrow();\n\n      // Reset element for afterEach\n      element = document.createElement('div');\n      element.classList.add('element');\n      document.body.appendChild(element);\n    });\n\n    it('should handle destroy when both element and target are removed from DOM', () => {\n      expect.assertions(2);\n\n      const tether = new Tether({\n        element: '.element',\n        target: '.target',\n        attachment: 'top left',\n        targetAttachment: 'top right'\n      });\n\n      tether.enable();\n\n      // Verify tether is enabled\n      expect(tether.enabled).toBe(true);\n\n      // Disable first to prevent any positioning attempts\n      tether.disable();\n\n      // Remove both from DOM before destroying tether\n      if (element.parentNode) {\n        element.parentNode.removeChild(element);\n      }\n      if (target.parentNode) {\n        target.parentNode.removeChild(target);\n      }\n\n      // This should not throw an error\n      expect(() => {\n        tether.destroy();\n      }).not.toThrow();\n\n      // Reset for afterEach\n      element = document.createElement('div');\n      element.classList.add('element');\n      document.body.appendChild(element);\n      target = document.createElement('div');\n      target.classList.add('target');\n      document.body.appendChild(target);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/utils/bounds.spec.js",
    "content": "import { getBounds, getScrollHandleBounds, getVisibleBounds, removeUtilElements } from '../../../src/js/utils/bounds';\n\ndescribe('Utils - bounds', () => {\n  let body;\n  let testElement;\n\n  beforeEach(() => {\n    body = document.body;\n    testElement = document.createElement('div');\n    testElement.style.position = 'absolute';\n    testElement.style.top = '100px';\n    testElement.style.left = '50px';\n    testElement.style.width = '200px';\n    testElement.style.height = '150px';\n    body.appendChild(testElement);\n  });\n\n  afterEach(() => {\n    if (testElement && testElement.parentNode) {\n      body.removeChild(testElement);\n    }\n    removeUtilElements(body);\n  });\n\n  describe('getBounds', () => {\n    it('returns bounds for a regular element', () => {\n      const bounds = getBounds(body, testElement);\n      expect(bounds).toBeDefined();\n      expect(typeof bounds.top).toBe('number');\n      expect(typeof bounds.left).toBe('number');\n      expect(typeof bounds.width).toBe('number');\n      expect(typeof bounds.height).toBe('number');\n    });\n\n    it('returns bounds for document element', () => {\n      const bounds = getBounds(body, document);\n      expect(bounds).toBeDefined();\n      expect(typeof bounds.top).toBe('number');\n      expect(typeof bounds.left).toBe('number');\n      expect(typeof bounds.width).toBe('number');\n      expect(typeof bounds.height).toBe('number');\n    });\n\n    it('calculates right and bottom properties', () => {\n      const bounds = getBounds(body, testElement);\n      expect(bounds.right).toBeDefined();\n      expect(bounds.bottom).toBeDefined();\n    });\n  });\n\n  describe('getScrollHandleBounds', () => {\n    it('returns scroll handle bounds for document.body', () => {\n      const bounds = getScrollHandleBounds(body, document.body);\n      expect(bounds).toBeDefined();\n      expect(bounds.width).toBe(15);\n      expect(typeof bounds.height).toBe('number');\n      expect(typeof bounds.left).toBe('number');\n      expect(typeof bounds.top).toBe('number');\n    });\n\n    it('returns scroll handle bounds for regular element', () => {\n      testElement.style.overflow = 'scroll';\n      testElement.scrollTop = 10;\n      const bounds = getScrollHandleBounds(body, testElement);\n      expect(bounds).toBeDefined();\n      expect(bounds.width).toBe(15);\n      expect(typeof bounds.height).toBe('number');\n    });\n\n    it('enforces minimum height of 24 for non-body elements', () => {\n      const smallElement = document.createElement('div');\n      smallElement.style.height = '50px';\n      smallElement.style.width = '100px';\n      smallElement.style.overflow = 'scroll';\n      smallElement.style.position = 'absolute';\n      smallElement.style.border = '1px solid black';\n      smallElement.innerHTML = '<div style=\"height: 200px;\"></div>';\n      body.appendChild(smallElement);\n      \n      // Force a reflow to ensure styles are applied\n      smallElement.offsetHeight;\n      \n      const bounds = getScrollHandleBounds(body, smallElement);\n      expect(typeof bounds.height).toBe('number');\n      expect(isNaN(bounds.height)).toBe(false);\n      // The calculation should enforce minimum of 24\n      expect(bounds.height).toBeGreaterThanOrEqual(24);\n      \n      body.removeChild(smallElement);\n    });\n\n    it('handles scrollable elements with scrollTop', () => {\n      const scrollableElement = document.createElement('div');\n      scrollableElement.style.height = '100px';\n      scrollableElement.style.overflow = 'scroll';\n      scrollableElement.innerHTML = '<div style=\"height: 500px;\"></div>';\n      body.appendChild(scrollableElement);\n      \n      scrollableElement.scrollTop = 50;\n      const bounds = getScrollHandleBounds(body, scrollableElement);\n      expect(bounds).toBeDefined();\n      expect(typeof bounds.top).toBe('number');\n      \n      body.removeChild(scrollableElement);\n    });\n  });\n\n  describe('getVisibleBounds', () => {\n    it('returns visible bounds for document.body', () => {\n      const bounds = getVisibleBounds(body, document.body);\n      expect(bounds).toBeDefined();\n      expect(bounds.top).toBe(window.pageYOffset);\n      expect(bounds.left).toBe(window.pageXOffset);\n      expect(bounds.height).toBe(window.innerHeight);\n      expect(bounds.width).toBe(window.innerWidth);\n    });\n\n    it('returns visible bounds for regular element', () => {\n      const bounds = getVisibleBounds(body, testElement);\n      expect(bounds).toBeDefined();\n      expect(typeof bounds.top).toBe('number');\n      expect(typeof bounds.left).toBe('number');\n      expect(typeof bounds.width).toBe('number');\n      expect(typeof bounds.height).toBe('number');\n    });\n\n    it('constrains bounds to viewport dimensions', () => {\n      const bounds = getVisibleBounds(body, testElement);\n      expect(bounds.height).toBeLessThanOrEqual(window.innerHeight);\n      expect(bounds.width).toBeLessThanOrEqual(window.innerWidth);\n    });\n\n    it('adjusts bounds when element is above viewport', () => {\n      testElement.style.top = '-100px';\n      const bounds = getVisibleBounds(body, testElement);\n      expect(bounds.top).toBeGreaterThanOrEqual(window.pageYOffset);\n    });\n\n    it('adjusts bounds when element is to the left of viewport', () => {\n      testElement.style.left = '-100px';\n      const bounds = getVisibleBounds(body, testElement);\n      expect(bounds.left).toBeGreaterThanOrEqual(window.pageXOffset);\n    });\n\n    it('subtracts 2 from height and width', () => {\n      const bounds = getVisibleBounds(body, testElement);\n      // The bounds should have 2 subtracted from both dimensions\n      expect(bounds.height).toBeLessThan(window.innerHeight);\n      expect(bounds.width).toBeLessThan(window.innerWidth);\n    });\n  });\n\n  describe('removeUtilElements', () => {\n    it('removes zero element from body', () => {\n      // First trigger getBounds which creates the zero element\n      getBounds(body, testElement);\n      \n      // Count elements with data-tether-id before removal\n      const elementsBefore = body.querySelectorAll('[data-tether-id]').length;\n      \n      removeUtilElements(body);\n      \n      // After removal, there should be fewer or zero elements with data-tether-id\n      const elementsAfter = body.querySelectorAll('[data-tether-id]').length;\n      expect(elementsAfter).toBeLessThanOrEqual(elementsBefore);\n    });\n\n    it('can be called multiple times safely', () => {\n      removeUtilElements(body);\n      expect(() => removeUtilElements(body)).not.toThrow();\n    });\n  });\n\n  describe('getBounds caching behavior', () => {\n    it('creates and reuses zero element for origin calculation', () => {\n      const bounds1 = getBounds(body, testElement);\n      const tetherElements1 = body.querySelectorAll('[data-tether-id]').length;\n      \n      const bounds2 = getBounds(body, testElement);\n      const tetherElements2 = body.querySelectorAll('[data-tether-id]').length;\n      \n      // Should reuse the same zero element\n      expect(tetherElements1).toBeGreaterThan(0);\n      expect(tetherElements2).toBe(tetherElements1);\n      expect(bounds1).toBeDefined();\n      expect(bounds2).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/utils/classes.spec.js",
    "content": "import { addClass, getClass, removeClass } from '../../../src/js/utils/classes';\n\ndescribe('Utils - classes', () => {\n  describe('addClass/removeClass', () => {\n    it('adds/removes classes from element', () => {\n      const element = document.createElement('div');\n      expect(element.classList.length, 'no classes').toEqual(0);\n\n      addClass(element, 'foo bar baz');\n\n      expect(element.classList.length, 'classes added').toEqual(3);\n      expect(element.classList.contains('foo'), 'has foo class').toBe(true);\n      expect(element.classList.contains('bar'), 'has bar class').toBe(true);\n      expect(element.classList.contains('baz'), 'has baz class').toBe(true);\n\n      removeClass(element, 'foo baz');\n\n      expect(element.classList.length, 'classes removed').toEqual(1);\n      expect(element.classList.contains('foo'), 'does not have foo class').toBe(false);\n      expect(element.classList.contains('bar'), 'has bar class').toBe(true);\n      expect(element.classList.contains('baz'), 'does not have baz class').toBe(false);\n    });\n  });\n\n  describe('getClass', () => {\n    it('returns the key if no other args passed', () => {\n      const keyClass = getClass('justKey');\n\n      expect(keyClass).toBe('justKey');\n    });\n\n    it('returns the key as the class, if no class by key', () => {\n      const classes = { noKey: 'aClassKey' };\n      const keyClass = getClass('justKey', classes);\n\n      expect(keyClass).toBe('justKey');\n    });\n\n    it('returns the existing declared class by key', () => {\n      const classes = { justKey: 'aClassKey' };\n      const keyClass = getClass('justKey', classes);\n\n      expect(keyClass).toBe('aClassKey');\n    });\n\n    it('returns the value if classes has the key as a truthy value', () => {\n      const classes = { justKey: 'otherValue' };\n      const keyClass = getClass('justKey', classes);\n\n      expect(keyClass).toBe('otherValue');\n    });\n\n    it('returns the empty string if classes has the key as a boolean false', () => {\n      const classes = { justKey: false };\n      const keyClass = getClass('justKey', classes);\n\n      expect(keyClass).toBe('');\n    });\n\n    it('returns the key with a classPrefix if no classes value for the key', () => {\n      const keyClass = getClass('justKey', {}, 'testPrefix');\n\n      expect(keyClass).toBe('testPrefix-justKey');\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/utils/deferred.spec.js",
    "content": "import { defer, flush } from '../../../src/js/utils/deferred';\nimport { stub } from 'sinon';\n\ndescribe('Utils - deferred', () => {\n  describe('defer/flush', () => {\n    it('calls deferred functions when flush is called', () => {\n      const stub1 = stub();\n      const stub2 = stub();\n      defer(stub1);\n      defer(stub2);\n      expect(stub1.called).toBe(false);\n      expect(stub2.called).toBe(false);\n      flush();\n      expect(stub1.called).toBe(true);\n      expect(stub2.called).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/utils/offset.spec.js",
    "content": "import { addOffset, attachmentToOffset, autoToFixedAttachment, offsetToPx, parseTopLeft } from '../../../src/js/utils/offset';\n\ndescribe('Utils - offset', () => {\n  describe('addOffset', () => {\n    it('offsets added together', () => {\n      const offset1 = { top: 20, left: 50 };\n      const offset2 = { top: 15, left: 10 };\n      expect(addOffset(offset1, offset2)).toStrictEqual({ left: 60, top: 35 });\n    });\n  });\n\n  describe('attachmentToOffset', () => {\n    it('top left', () => {\n      expect(attachmentToOffset({ left: 'left', top: 'top' })).toStrictEqual({ left: 0, top: 0 });\n    });\n\n    it('middle center', () => {\n      expect(attachmentToOffset({ left: 'center', top: 'middle' })).toStrictEqual({ left: '50%', top: '50%' });\n    });\n\n    it('bottom right', () => {\n      expect(attachmentToOffset({ left: 'right', top: 'bottom' })).toStrictEqual({ left: '100%', top: '100%' });\n    });\n  });\n\n  describe('autoToFixedAttachment', () => {\n    it('mirror left', () => {\n      expect(autoToFixedAttachment(\n        { left: 'auto', top: 'top' },\n        { left: 'left', top: 'top' }\n      )).toStrictEqual({ left: 'right', top: 'top' });\n    });\n\n    it('mirror center', () => {\n      expect(autoToFixedAttachment(\n        { left: 'auto', top: 'top' },\n        { left: 'center', top: 'top' }\n      )).toStrictEqual({ left: 'center', top: 'top' });\n    });\n\n    it('mirror right', () => {\n      expect(autoToFixedAttachment(\n        { left: 'auto', top: 'top' },\n        { left: 'right', top: 'top' }\n      )).toStrictEqual({ left: 'left', top: 'top' });\n    });\n\n    it('mirror top', () => {\n      expect(autoToFixedAttachment(\n        { left: 'left', top: 'auto' },\n        { left: 'left', top: 'top' }\n      )).toStrictEqual({ left: 'left', top: 'bottom' });\n    });\n\n    it('mirror middle', () => {\n      expect(autoToFixedAttachment(\n        { left: 'left', top: 'auto' },\n        { left: 'left', top: 'middle' }\n      )).toStrictEqual({ left: 'left', top: 'middle' });\n    });\n\n    it('mirror bottom', () => {\n      expect(autoToFixedAttachment(\n        { left: 'left', top: 'auto' },\n        { left: 'left', top: 'bottom' }\n      )).toStrictEqual({ left: 'left', top: 'top' });\n    });\n  });\n\n  describe('offsetToPx', () => {\n    it('calculates px from %', () => {\n      const offset = { left: '30%', top: '20%' };\n      const size = { height: 1000, width: 1000 };\n      expect(offsetToPx(offset, size)).toStrictEqual({ left: 300, top: 200 });\n    });\n  });\n\n  describe('parseTopLeft', () => {\n    it('splits string to get top/left', () => {\n      expect(parseTopLeft('50 100')).toStrictEqual({ left: '100', top: '50' });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/utils/parents.spec.js",
    "content": "import { getScrollParents, getOffsetParent } from '../../../src/js/utils/parents';\n\ndescribe('Utils - parents', () => {\n  let body;\n  let testElement;\n\n  beforeEach(() => {\n    body = document.body;\n  });\n\n  afterEach(() => {\n    if (testElement && testElement.parentNode) {\n      testElement.parentNode.removeChild(testElement);\n    }\n  });\n\n  describe('getScrollParents', () => {\n    it('returns array with element itself for fixed position', () => {\n      testElement = document.createElement('div');\n      testElement.style.position = 'fixed';\n      body.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toEqual([testElement]);\n    });\n\n    it('includes body in scroll parents', () => {\n      testElement = document.createElement('div');\n      body.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(document.body);\n    });\n\n    it('finds scrollable parent with overflow auto', () => {\n      const scrollParent = document.createElement('div');\n      scrollParent.style.overflow = 'auto';\n      scrollParent.style.height = '100px';\n      body.appendChild(scrollParent);\n      \n      testElement = document.createElement('div');\n      scrollParent.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(scrollParent);\n      \n      body.removeChild(scrollParent);\n    });\n\n    it('finds scrollable parent with overflow scroll', () => {\n      const scrollParent = document.createElement('div');\n      scrollParent.style.overflow = 'scroll';\n      scrollParent.style.height = '100px';\n      body.appendChild(scrollParent);\n      \n      testElement = document.createElement('div');\n      scrollParent.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(scrollParent);\n      \n      body.removeChild(scrollParent);\n    });\n\n    it('finds scrollable parent with overflowY scroll', () => {\n      const scrollParent = document.createElement('div');\n      scrollParent.style.overflowY = 'scroll';\n      scrollParent.style.height = '100px';\n      body.appendChild(scrollParent);\n      \n      testElement = document.createElement('div');\n      scrollParent.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(scrollParent);\n      \n      body.removeChild(scrollParent);\n    });\n\n    it('finds scrollable parent with overflowX scroll', () => {\n      const scrollParent = document.createElement('div');\n      scrollParent.style.overflowX = 'scroll';\n      scrollParent.style.width = '100px';\n      body.appendChild(scrollParent);\n      \n      testElement = document.createElement('div');\n      scrollParent.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(scrollParent);\n      \n      body.removeChild(scrollParent);\n    });\n\n    it('finds scrollable parent with overflow overlay', () => {\n      const scrollParent = document.createElement('div');\n      scrollParent.style.overflow = 'overlay';\n      scrollParent.style.height = '100px';\n      body.appendChild(scrollParent);\n      \n      testElement = document.createElement('div');\n      scrollParent.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(scrollParent);\n      \n      body.removeChild(scrollParent);\n    });\n\n    it('handles absolute positioning correctly', () => {\n      const relativeParent = document.createElement('div');\n      relativeParent.style.position = 'relative';\n      relativeParent.style.overflow = 'auto';\n      body.appendChild(relativeParent);\n      \n      testElement = document.createElement('div');\n      testElement.style.position = 'absolute';\n      relativeParent.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(relativeParent);\n      \n      body.removeChild(relativeParent);\n    });\n\n    it('skips non-positioned scrollable parents for absolute elements', () => {\n      const staticParent = document.createElement('div');\n      staticParent.style.overflow = 'auto';\n      staticParent.style.position = 'static';\n      body.appendChild(staticParent);\n      \n      testElement = document.createElement('div');\n      testElement.style.position = 'absolute';\n      staticParent.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      // Static positioned parent should not be included for absolutely positioned children\n      expect(parents).not.toContain(staticParent);\n      \n      body.removeChild(staticParent);\n    });\n\n    it('handles nested scroll parents', () => {\n      const outerScroll = document.createElement('div');\n      outerScroll.style.overflow = 'auto';\n      outerScroll.style.height = '200px';\n      body.appendChild(outerScroll);\n      \n      const innerScroll = document.createElement('div');\n      innerScroll.style.overflow = 'scroll';\n      innerScroll.style.height = '100px';\n      outerScroll.appendChild(innerScroll);\n      \n      testElement = document.createElement('div');\n      innerScroll.appendChild(testElement);\n      \n      const parents = getScrollParents(testElement);\n      expect(parents).toContain(innerScroll);\n      expect(parents).toContain(outerScroll);\n      \n      body.removeChild(outerScroll);\n    });\n\n    it('handles elements where getComputedStyle returns null', () => {\n      testElement = document.createElement('div');\n      body.appendChild(testElement);\n      \n      // Create a mock scenario - in Firefox, elements in display:none iframes return null\n      const parentWithNullStyle = document.createElement('div');\n      body.appendChild(parentWithNullStyle);\n      parentWithNullStyle.appendChild(testElement);\n      \n      expect(() => {\n        const parents = getScrollParents(testElement);\n        expect(Array.isArray(parents)).toBe(true);\n      }).not.toThrow();\n      \n      body.removeChild(parentWithNullStyle);\n    });\n  });\n\n  describe('getOffsetParent', () => {\n    it('returns offsetParent for regular element', () => {\n      testElement = document.createElement('div');\n      body.appendChild(testElement);\n      \n      const offsetParent = getOffsetParent(testElement);\n      expect(offsetParent).toBeDefined();\n    });\n\n    it('returns document.documentElement when offsetParent is null', () => {\n      testElement = document.createElement('div');\n      testElement.style.position = 'fixed';\n      body.appendChild(testElement);\n      \n      const offsetParent = getOffsetParent(testElement);\n      // Fixed elements typically have null offsetParent, should return documentElement\n      expect(offsetParent).toBeDefined();\n    });\n\n    it('returns positioned parent as offsetParent', () => {\n      const positionedParent = document.createElement('div');\n      positionedParent.style.position = 'relative';\n      body.appendChild(positionedParent);\n      \n      testElement = document.createElement('div');\n      positionedParent.appendChild(testElement);\n      \n      const offsetParent = getOffsetParent(testElement);\n      expect([positionedParent, document.documentElement, document.body]).toContain(offsetParent);\n      \n      body.removeChild(positionedParent);\n    });\n  });\n});\n"
  }
]