├── .browserslistrc ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .hsdoc ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── .release-it.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── README.md ├── RELEASE.md ├── __mocks__ └── styleMock.js ├── babel.config.js ├── cypress.config.cjs ├── docs ├── 1-Overview │ ├── 1-why_you_should_use_tether.md │ ├── 2-repositioning.md │ └── 3-why_we_dont_support_IE_8.md ├── 2-Examples │ ├── 1-list_of_examples.md │ └── 2-projects_using_tether.md ├── 3-Advanced │ ├── 1-embedding_tether.md │ └── 2-extending_tether.md ├── css │ └── intro.css ├── intro.md ├── js │ └── intro.js ├── sass │ └── intro.sass └── welcome │ ├── browser-demo.html │ ├── css │ ├── browser-demo.css │ ├── prism.css │ └── welcome.css │ ├── index.html │ ├── js │ ├── drop.js │ ├── jquery.js │ ├── log.js │ └── welcome.js │ └── sass │ ├── _inline-block.sass │ ├── browser-demo.sass │ └── welcome.sass ├── examples ├── common │ └── css │ │ └── style.css ├── content-visible │ └── index.html ├── dolls │ ├── dolls.css │ ├── dolls.js │ └── index.html ├── element-scroll │ └── index.html ├── enable-disable │ └── index.html ├── index.html ├── out-of-bounds │ └── index.html ├── pin │ └── index.html ├── resources │ └── css │ │ └── base.css ├── scroll │ └── index.html ├── simple │ └── index.html ├── testbed │ └── index.html └── viewport │ ├── colors.css │ └── index.html ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── css │ ├── helpers │ │ ├── _tether-theme-arrows.scss │ │ ├── _tether-theme-basic.scss │ │ └── _tether.scss │ ├── mixins │ │ ├── _inline-block.scss │ │ └── _pie-clearfix.scss │ ├── tether-theme-arrows-dark.scss │ ├── tether-theme-arrows.scss │ ├── tether-theme-basic.scss │ └── tether.scss └── js │ ├── abutment.js │ ├── constraint.js │ ├── evented.js │ ├── shift.js │ ├── tether.js │ └── utils │ ├── bounds.js │ ├── classes.js │ ├── deferred.js │ ├── general.js │ ├── offset.js │ ├── parents.js │ └── type-check.js ├── test ├── .eslintrc.js ├── cypress │ ├── integration │ │ ├── content-visible.cy.js │ │ ├── enable-disable.cy.js │ │ ├── out-of-bounds.cy.js │ │ ├── pin.cy.js │ │ ├── scroll.cy.js │ │ ├── simple.cy.js │ │ └── testbed.cy.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── constraint.spec.js │ ├── evented.spec.js │ ├── setupTests.js │ ├── tether.spec.js │ └── utils │ ├── classes.spec.js │ ├── deferred.spec.js │ └── offset.spec.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | last 2 Chrome versions 4 | last 2 Firefox versions 5 | last 2 Safari versions 6 | last 2 Edge versions 7 | ie >= 10 8 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 10 6 | method-count: 7 | config: 8 | threshold: 25 9 | method-lines: 10 | config: 11 | threshold: 30 12 | exclude_patterns: 13 | - "demo/" 14 | - "dist/" 15 | - "docs/" 16 | - "examples/" 17 | - "jsdoc-template/" 18 | - "**/node_modules/" 19 | - "**/test/" 20 | - "**/tests/" 21 | - "**/vendor/" 22 | - "babel.config.js" 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /dist/ 3 | /docs/ 4 | /examples/ 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | sourceType: 'module' 6 | }, 7 | extends: [ 8 | 'eslint:recommended' 9 | ], 10 | env: { 11 | browser: true, 12 | es6: true 13 | }, 14 | rules: { 15 | 'complexity': ['warn', 6], 16 | 'max-lines': ['warn', { max: 250, skipBlankLines: true, skipComments: true }], 17 | 'no-console': 'off', 18 | 'no-unused-vars': 'off', 19 | 'prefer-const': 'off' 20 | }, 21 | overrides: [ 22 | // node files 23 | { 24 | files: [ 25 | '.eslintrc.js', 26 | 'babel.config.js', 27 | 'jest.config.js', 28 | 'rollup.config.js', 29 | '__mocks__/styleMock.js' 30 | ], 31 | parserOptions: { 32 | sourceType: 'module', 33 | ecmaVersion: 2020 34 | }, 35 | env: { 36 | node: true 37 | } 38 | } 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | labels: 10 | - dependencies 11 | versioning-strategy: increase 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | 2 | name: CI Build 3 | 4 | on: 5 | pull_request: {} 6 | push: 7 | branches: 8 | - master 9 | tags: 10 | - v* 11 | 12 | jobs: 13 | test: 14 | name: Tests 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: volta-cli/action@v1 20 | with: 21 | node-version: 16.x 22 | - run: yarn 23 | - run: yarn test 24 | 25 | automerge: 26 | needs: [test] 27 | runs-on: ubuntu-latest 28 | permissions: 29 | pull-requests: write 30 | contents: write 31 | steps: 32 | - uses: fastify/github-action-merge-dependabot@v3.2.0 33 | with: 34 | github-token: ${{secrets.GITHUB_TOKEN}} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | /.idea/ 3 | /.vscode/ 4 | 5 | /.log/ 6 | /.nyc_output/ 7 | /coverage/ 8 | /cypress/ 9 | /docs/ 10 | /dist/ 11 | /node_modules/ 12 | /test/unit/dist 13 | /.DS_Store 14 | /.sass-cache 15 | /npm-debug.log* 16 | /stats.html 17 | /yarn-error.log 18 | -------------------------------------------------------------------------------- /.hsdoc: -------------------------------------------------------------------------------- 1 | name: "Tether" 2 | description: "Marrying DOM elements for life" 3 | domain: "tetherjs.dev" 4 | source: "src/**/*.js" 5 | examples: "**/*.md" 6 | assets: "{dist/js/*.js,dist/css/*.css,docs/css/*.css,docs/js/*,js,docs/welcome/*,examples/*}" 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | 4 | coverage/ 5 | cypress/ 6 | docs/ 7 | examples/ 8 | esdoc/ 9 | jsdoc-template/ 10 | test/ 11 | tests/ 12 | 13 | .codeclimate.yml 14 | .eslintignore 15 | .eslintrc.js 16 | .gitignore 17 | .hsdoc 18 | .stylelintrc.js 19 | .travis.yml 20 | babel.config.js 21 | cypress.json 22 | index.html 23 | rollup.config.js 24 | yarn.lock 25 | yarn-error.log 26 | 27 | CONTRIBUTING.md 28 | HISTORY.md 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | trailingComma: 'none' 6 | }; -------------------------------------------------------------------------------- /.release-it.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | hooks: { 5 | 'after:bump': 'yarn build' 6 | }, 7 | plugins: { 8 | 'release-it-lerna-changelog': { 9 | infile: 'CHANGELOG.md', 10 | launchEditor: true 11 | } 12 | }, 13 | git: { 14 | tagName: 'v${version}' 15 | }, 16 | github: { 17 | release: true, 18 | tokenRef: 'GITHUB_AUTH', 19 | assets: ['dist/**/*.css', 'dist/**/*.js', 'dist/**/*.map'] 20 | }, 21 | npm: { 22 | publish: true 23 | } 24 | }; -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | You will need: 4 | 5 | - [Yarn](https://yarnpkg.com/) 6 | 7 | Windows users will need additional setup to enable build capabilities in NPM. 8 | From an administrative command window: 9 | 10 | ```sh 11 | yarn global add windows-build-tools 12 | ``` 13 | 14 | ## Getting started 15 | 16 | 1. Fork the project 17 | 2. Clone your forked project by running `git clone git@github.com:{ 18 | YOUR_USERNAME }/tether.git` 19 | 3. Run `yarn` to install node modules 20 | 4. Test that you can build the source by running `yarn build` and ensure the `dist` directory appears. 21 | 22 | ## Writing code! 23 | 24 | We use `rollup` to facilitate things like transpilation, minification, etc. so 25 | you can focus on writing relevant code. If there is a fix or feature you would like 26 | to contribute, we ask that you take the following steps: 27 | 28 | 1. Most of the _editable_ code lives in the `src` directory while built code 29 | will end up in the `dist` directory upon running `yarn build`. 30 | 31 | 2. Some examples are served out of the `examples` directory. Running `yarn start` will open the list in your browser and initiate a live-reloading session as you make changes. 32 | 33 | 34 | ## Opening Pull Requests 35 | 36 | 1. Please Provide a thoughtful commit message and push your changes to your fork using 37 | `git push origin master` (assuming your forked project is using `origin` for 38 | the remote name and you are on the `master` branch). 39 | 40 | 2. Open a Pull Request on GitHub with a description of your changes. 41 | 42 | 43 | ## Testing 44 | 45 | All PRs, that change code functionality, are required to have accompanying tests. 46 | 47 | ### Acceptance Tests 48 | 49 | Acceptance 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 `yarn test:ci:watch` to build your latest changes and begin running the tests inside a Chrome browser instance. 50 | 51 | ⚠️ 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. 52 | 53 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## v1.3.0 2 | - Tether instances now fire an 'update' event when attachments change due to constraints (#119) 3 | 4 | ## v1.0.1 5 | - Update arrow mixin to change arrow pointer event 6 | 7 | 8 | ## v1.0.0 9 | - Coffeescript -> ES6 10 | - Proper UMD Wrapper 11 | - Update build steps 12 | - Add changelog 13 | - Provide minified CSS 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2019 HubSpot, Inc. 2 | Copyright (c) 2019-2022 Ship Shape Consulting LLC 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tether 2 | 3 |
4 | 5 | Ship Shape 6 | 7 | 8 | **[Tether is maintained by Ship Shape. Contact us for web app consulting, development, and training for your project](https://shipshape.io/services/app-development/)**. 9 |
10 | 11 | [![npm version](https://badge.fury.io/js/tether.svg)](http://badge.fury.io/js/tether) 12 | ![Download count all time](https://img.shields.io/npm/dt/tether.svg) 13 | [![npm](https://img.shields.io/npm/dm/tether.svg)]() 14 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/tether.svg) 15 | [![Build Status](https://travis-ci.com/shipshapecode/tether.svg?branch=master)](https://travis-ci.com/shipshapecode/tether) 16 | [![Maintainability](https://api.codeclimate.com/v1/badges/57016ae28b99490eac30/maintainability)](https://codeclimate.com/github/shipshapecode/tether/maintainability) 17 | [![Test Coverage](https://api.codeclimate.com/v1/badges/57016ae28b99490eac30/test_coverage)](https://codeclimate.com/github/shipshapecode/tether/test_coverage) 18 | 19 | ## 🐙 Project status 🐙 20 | 21 | We at Ship Shape have recently taken over Tether's maintenance and hope to modernize and revitalize it. Stay tuned for updates! 22 | 23 | ## Install 24 | 25 | __npm__ 26 | ```sh 27 | npm install tether 28 | ``` 29 | 30 | For the latest beta: 31 | 32 | ```sh 33 | npm install tether@next 34 | ``` 35 | 36 | __download__ 37 | 38 | Or download from the [releases](https://github.com/shipshapecode/tether/releases). 39 | 40 | ## Introduction 41 | 42 | [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. 43 | 44 | There 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. 45 | 46 | Some 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). 47 | 48 | Please have a look at the [documentation](http://tetherjs.dev/) for a more detailed explanation of why you might need Tether for your next project. 49 | 50 | ## What to Use Tether for and When to Use It 51 | 52 | Tether 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. 53 | 54 | Tether’s goal to is to help you position your elements side-by-side when needed. 55 | 56 | Let’s say you’ve started working on your dream project—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. 57 | 58 | Your HTML code might look something like this: 59 | 60 | ```html 61 |
62 | Awesome Picture 63 |
64 | ... 65 |
66 |
67 | ``` 68 | 69 | Now, 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. 70 | 71 | Not 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. 72 | 73 | **Enter Tether!** 74 | 75 | After installing Tether and including it in your project, you can begin using it! 76 | 77 | 1. In your JavaScript file, create a new instance (or constructor function) of the `Tether` object: 78 | 79 | ```javascript 80 | new Tether({}); 81 | ``` 82 | 83 | 2. 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/). 84 | 85 | ```javascript 86 | new Tether({ 87 | element: '.comments', 88 | target: '.picture', 89 | attachment: 'top right', 90 | targetAttachment: 'top left' 91 | }); 92 | ``` 93 | 94 | Now 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. 95 | 96 | There are tons of other useful features of Tether as well, instead of “comment boxes” you could also build: 97 | 98 | * Tooltips for useful hints and tricks, 99 | * Dropdown menus, 100 | * Autocomplete popups for forms, 101 | * and [more](http://tetherjs.dev/examples/list_of_examples/)! 102 | 103 | ## Usage 104 | You only need to include `tether.min.js` in your page: 105 | ``` 106 | 107 | ``` 108 | Or use a CDN: 109 | ``` 110 | 111 | ``` 112 | 113 | The css files are not required to get tether running. 114 | 115 | For more details jump straight in to the detailed [Usage](http://tetherjs.dev/#usage) page. 116 | 117 | [![Tether Docs](http://i.imgur.com/YCx8cLr.png)](http://tetherjs.dev/#usage) 118 | 119 | [Demo & API Documentation](http://tetherjs.dev/) 120 | 121 | ## Contributing 122 | 123 | We encourage contributions of all kinds. If you would like to contribute in some way, please review our [guidelines for contributing](CONTRIBUTING.md). 124 | 125 | ## License 126 | Copyright © 2019-2022 Ship Shape Consulting LLC - [MIT License](LICENSE) 127 | Copyright © 2014-2018 HubSpot - [MIT License](LICENSE) 128 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | ## Preparation 8 | 9 | Since the majority of the actual release process is automated, the primary 10 | remaining task prior to releasing is confirming that all pull requests that 11 | have been merged since the last release have been labeled with the appropriate 12 | `lerna-changelog` labels and the titles have been updated to ensure they 13 | represent something that would make sense to our users. Some great information 14 | on why this is important can be found at 15 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 16 | guiding principle here is that changelogs are for humans, not machines. 17 | 18 | When reviewing merged PR's the labels to be used are: 19 | 20 | * breaking - Used when the PR is considered a breaking change. 21 | * enhancement - Used when the PR adds a new feature or enhancement. 22 | * bug - Used when the PR fixes a bug included in a previous release. 23 | * documentation - Used when the PR adds or updates documentation. 24 | * internal - Used for internal changes that still require a mention in the 25 | changelog/release notes. 26 | 27 | ## Release 28 | 29 | Once the prep work is completed, the actual release is straight forward: 30 | 31 | * First, ensure that you have installed your projects dependencies: 32 | 33 | ```sh 34 | yarn install 35 | ``` 36 | 37 | * Second, ensure that you have obtained a 38 | [GitHub personal access token][generate-token] with the `repo` scope (no 39 | other permissions are needed). Make sure the token is available as the 40 | `GITHUB_AUTH` environment variable. 41 | 42 | For instance: 43 | 44 | ```bash 45 | export GITHUB_AUTH=abc123def456 46 | ``` 47 | 48 | [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable 49 | 50 | * And last (but not least 😁) do your release. 51 | 52 | ```sh 53 | npx release-it 54 | ``` 55 | 56 | [release-it](https://github.com/release-it/release-it/) manages the actual 57 | release process. It will prompt you to to choose the version number after which 58 | you will have the chance to hand tweak the changelog to be used (for the 59 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 60 | pushing the tag and commits, etc. 61 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | return { 5 | env: { 6 | development: { 7 | presets: [ 8 | [ 9 | '@babel/preset-env', 10 | { 11 | loose: true 12 | } 13 | ] 14 | ] 15 | }, 16 | test: { 17 | presets: [ 18 | [ 19 | '@babel/preset-env' 20 | ] 21 | ], 22 | plugins: [ 23 | 'babel-plugin-rewire', 24 | 'transform-es2015-modules-commonjs' 25 | ] 26 | } 27 | } 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /cypress.config.cjs: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: 'test/cypress/fixtures', 5 | video: false, 6 | e2e: { 7 | // We've imported your old cypress plugins here. 8 | // You may want to clean this up later by importing these. 9 | setupNodeEvents(on, config) { 10 | return require('./test/cypress/plugins/index.js')(on, config) 11 | }, 12 | baseUrl: 'http://localhost:9002', 13 | specPattern: 'test/cypress/integration/**/*.cy.{js,jsx,ts,tsx}', 14 | supportFile: 'test/cypress/support/index.js', 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /docs/1-Overview/1-why_you_should_use_tether.md: -------------------------------------------------------------------------------- 1 | ## Why You Should Use Tether 2 | 3 | Virtually every app includes some sort of overlay attached to an element on the page. 4 | Things like [tooltips](http://github.hubspot.com/tooltip/docs/welcome), 5 | [dropdowns](http://github.hubspot.com/select/docs/welcome), [hover-activated info boxes](http://github.hubspot.com/drop/docs/welcome), etc. 6 | 7 | Those elements need to be attached to something on the page. Actually placing them next to 8 | the element in the DOM causes problems though, if any parent element is anything 9 | but `overflow: visible`, the element gets cut off. So you need absolute positioning 10 | in the body. 11 | 12 | Some of the time absolute positioning is right, but what about if the thing we're 13 | attached to is fixed to the center of the screen? We'll have to move it every 14 | time the user scrolls. What about if the element is in a scrollable container, 15 | if the overlay is inside of it (so no clipping), it would be cool if the code 16 | were smart enough to move it inside when that area is scrolled. That way we 17 | need to reposition it even less. 18 | 19 | It would also be nice if the code could somehow figure out whether positioning it 20 | from the top, bottom, left, or right would result in the fewest repositionings 21 | as the user scrolls or resizes. 22 | 23 | Most of the time you're building these elements it would be nice for the element to 24 | flip to the other side of the element if it hits the edge of the screen, or a scrollable 25 | container it might be in. It would be nice if we could confine the element 26 | to within some area, or even hide it when it leaves. 27 | 28 | It would be nice for the element to be repositioned with CSS transforms 29 | rather than top and left when possible, to allow the positioning to be done entirely 30 | in the GPU. 31 | 32 | Now that the positioning is so fancy, you're going to use it for more and more 33 | elements. It would be cool if the library could optimize all of their repositioning 34 | into a single repaint. 35 | 36 | All of that is baked into Tether. 37 | 38 | ### tl;dr 39 | 40 | - Optimized GPU-accelerated repositioning for 60fps scrolling 41 | - Reliable positioning on any possible corner, edge or point in between. 42 | - Support for repositioning or pinning the element when it would be offscreen 43 | - Designed to be embeddable in other libraries 44 | -------------------------------------------------------------------------------- /docs/1-Overview/2-repositioning.md: -------------------------------------------------------------------------------- 1 | Repositioning 2 | ----- 3 | 4 | Tethers will be automatically repositioned when the page is resized, and when any element containing the Tether is scrolled. 5 | If the element moves for some other reason (e.g. with JavaScript), Tether won't know to reposition the element. 6 | 7 | #### Manually Repositioning 8 | 9 | The simplest way to reposition every Tether on the page is to call `Tether.position()`. It will efficiently reposition every 10 | Tether in a single repaint, making it more efficient than manually repositioning many Tethers individually. 11 | 12 | ```javascript 13 | Tether.position() 14 | ``` 15 | 16 | #### Repositioning a Single Tether 17 | 18 | If you have many Tethers on screen, it may be more efficient to just reposition the tether that needs it. You can do this 19 | by calling the `.position` method on the Tether instance: 20 | 21 | ```javascript 22 | tether = new Tether({ ... }) 23 | 24 | // Later: 25 | tether.position() 26 | ``` 27 | 28 | #### Tethering Hidden Elements 29 | 30 | If you are creating a tether involving elements which are `display: none`, or not actually in the DOM, 31 | your Tether may not be able to position itself properly. One way around this is to 32 | ensure that a position call happens after all layouts have finished: 33 | 34 | ```javascript 35 | myElement.style.display = 'block' 36 | 37 | tether = new Tether({ ... }) 38 | 39 | setTimeout(function(){ 40 | tether.position(); 41 | }) 42 | ``` 43 | 44 | In general however, you shouldn't have any trouble if both the element and the target are visible and in the DOM when you 45 | create the Tether. If that is not the case, create the Tether disabled (option `enabled`: `false`), and enable it when 46 | the elements are ready. 47 | -------------------------------------------------------------------------------- /docs/1-Overview/3-why_we_dont_support_IE_8.md: -------------------------------------------------------------------------------- 1 | Why we don't support IE 8 2 | ------------------------- 3 | 4 | We've been living in 2007 for a while now, pretending that new browser features don't 5 | exist because they aren't in IE8. You might not even know about some of these features, 6 | or think they are only enabled by jQuery or underscore, simply because it hasn't 7 | been an option to rely upon them. 8 | 9 | Here is the list of features you don't have if you choose to support IE 8: 10 | 11 | - HTML5 audio and video 12 | - SVG 13 | - Canvas 14 | - TrueType fonts 15 | - Media Queries 16 | - CSS Transforms 17 | - Multiple Backgrounds 18 | - CSS3 Units (vh, vw, rem) 19 | - Custom DOM events 20 | - Hardware accelerated graphics 21 | - The DOMContentLoaded event 22 | - addEventListener 23 | - Object.create, .seal, .freeze, .defineProperty 24 | - Array.isArray, .indexOf, .every, .some, .forEach, .map, .filter, .reduce 25 | - A modern JavaScript engine 26 | - Real developer tools 27 | - A consistent box model 28 | - jQuery 2 29 | - Google Apps 30 | - Tether 31 | 32 | It's true that IE 8 still holds a big chunk of the browsing population, but the reasons 33 | why they can't update are dwindling. There are two big reasons for continuing IE 8 support. 34 | 35 | #### Enterprises 36 | 37 | Microsoft is dropping support for XP in April, organizations who want security updates will have to upgrade. 38 | 39 | #### China uses XP 40 | 41 | Chrome, Firefox and Opera all support XP. Nothing prevents users from upgrading, except the inertia of 42 | organizations who still support IE 8. 43 | 44 | #### The Future 45 | 46 | We are skating towards where the puck will be, and we hope that as you decide to drop IE 8 support, 47 | you choose to add Tether to the list of awesome things you can do. 48 | -------------------------------------------------------------------------------- /docs/2-Examples/1-list_of_examples.md: -------------------------------------------------------------------------------- 1 | ### Examples 2 | 3 | It's our goal to create a wide variety of examples of how Tether 4 | can be used. Here's what we have so far, please send a PR with 5 | any examples you might create. 6 | 7 | #### Beginner 8 | 9 | - [simple](../../examples/simple): A simple example to get you started 10 | - [out-of-bounds](../../examples/out-of-bounds): How to hide the element when it would 11 | otherwise be offscreen 12 | - [pin](../../examples/pin): How to pin the element so it never goes offscreen 13 | - [enable-disable](../../examples/enable-disable): How to enable and disable the Tether 14 | in JavaScript 15 | 16 | #### Advanced 17 | 18 | - [content-visible](../../examples/content-visible): Demonstrates using the `'visible'` 19 | `targetModifier` to align an element with the visible portion of another. 20 | - [dolls](../../examples/dolls): A performance test to show several dozen elements, 21 | each tethered to the previous. Try dragging the top left tether. 22 | - [element-scroll](../../examples/element-scroll): Demonstrates using the `'scroll-handle'` 23 | `targetModifier` to align an element with the scrollbar of an element. 24 | - [scroll](../../examples/scroll): Demonstrates using the `'scroll-handle'` `targetModifier` 25 | to align an element with the body's scroll handle. 26 | - [viewport](../../examples/viewport): Demonstrates aligning an element with the 27 | viewport by using the `'visible'` `targetModifier` when tethered to the body. 28 | -------------------------------------------------------------------------------- /docs/2-Examples/2-projects_using_tether.md: -------------------------------------------------------------------------------- 1 | ## Projects Using Tether 2 | 3 | Here at HubSpot we have built a bunch of libraries on top of Tether, 4 | both because we wanted Tether-performance, and because we saw opportunities 5 | to improve on what was available in the client-side ecosystem. 6 | 7 | ### [Select](http://github.hubspot.com/select/docs/welcome) 8 | 9 | Select is a replacement for native browser select elements that is fully stylable. 10 | 11 | ### [Shepherd](http://github.hubspot.com/shepherd/docs/welcome) 12 | 13 | Shepherd is a library for making tours of your app to help onboard users and show off 14 | new features. 15 | 16 | ### [Tooltip](http://github.hubspot.com/tooltip/docs/welcome) 17 | 18 | A simple, easy-to-use implementation of tooltips that works well. 19 | 20 | ### [Drop](http://github.hubspot.com/drop/docs/welcome) 21 | 22 | Where Tether does general-purpose positioning, Drop assumes that you are interested 23 | in making something which pops up next to something the user clicks or hovers on. 24 | 25 | If you're building something that fits that pattern, Drop can make things a little easier. 26 | 27 | ### [Blueprint](http://blueprintjs.com/) 28 | 29 | A React UI toolkit for the web. 30 | 31 | ### [Ember Tether](https://github.com/yapplabs/ember-tether) 32 | 33 | An Ember.js-friendly interface for tether. 34 | 35 | ### [React Datepicker](https://github.com/Hacker0x01/react-datepicker) 36 | 37 | A simple and reusable datepicker component for React 38 | 39 | ### [reactstrap](https://reactstrap.github.io) 40 | 41 | Easy to use React Bootstrap 4 components. Tooltips & Popovers are powered by Tether. Advanced positioning of Dropdowns are supported via Tether. 42 | 43 | ### Your Project Here 44 | 45 | If you have a cool open-source library built on Tether, PR this doc. 46 | -------------------------------------------------------------------------------- /docs/3-Advanced/1-embedding_tether.md: -------------------------------------------------------------------------------- 1 | ## Embedding Tether 2 | 3 | Tether is designed to be embeddable in other libraries. 4 | 5 | There is one thing you should think about doing to create an embedded Tether: 6 | 7 | - Set the `classPrefix` of the tethers you create. That prefix will replace `'tether'` in 8 | all of the classes. You can also disable classes you don't intend on using with the `classes` 9 | option. 10 | -------------------------------------------------------------------------------- /docs/3-Advanced/2-extending_tether.md: -------------------------------------------------------------------------------- 1 | Extending Tether 2 | ----- 3 | 4 | Tether has a module system which can be used to modify Tether's positioning, or just do something each time the Tether is moved. 5 | 6 | Tether has an array called `Tether.modules`, push onto it to add a module: 7 | 8 | ```coffeescript 9 | Tether.modules.push 10 | position: ({top, left}) -> 11 | top += 10 12 | 13 | {top, left} 14 | ``` 15 | 16 | #### Position 17 | 18 | Your position function can either return a new object with `top` and `left`, `null`/`undefined` to leave the coordinates unchanged, or 19 | `false` to cancel the positioning. 20 | 21 | The position function is passed an object with the following elements: 22 | 23 | ```javascript 24 | { 25 | left, // The element's new position, from the top left corner of the page 26 | top, 27 | targetAttachment, // The targetAttachment, with 'auto' resolved to an actual attachment 28 | targetPos, // The coordinates of the target 29 | attachment, // The attachment, as passed in the option 30 | elementPos, // The coordinates of the element 31 | offset, // The offset, after it's converted into pixels and the manual offset is added 32 | targetOffset, // The attachment is converted into an offset and is included in these values 33 | manualOffset, // The manual offset, in pixels 34 | manualTargetOffset 35 | } 36 | ``` 37 | 38 | It is called with the Tether instance as its context (`this`). 39 | 40 | #### Initialize 41 | 42 | Modules can also have an `initialize` function which will be called when a new tether is created. The initialize function 43 | is also called with the Tether instance as its context. 44 | 45 | ```coffeescript 46 | Tether.modules.push 47 | initialize: -> 48 | console.log "New Tether Created!", @ 49 | ``` 50 | 51 | #### Examples 52 | 53 | [Constraints](https://github.com/HubSpot/tether/blob/master/src/js/constraint.js) and [shift](https://github.com/HubSpot/tether/blob/master/src/js/shift.js) are both implemented as modules. 54 | [Mark Attachment](https://github.com/HubSpot/tether/blob/master/src/js/markAttachment.js) is used by the docs. 55 | -------------------------------------------------------------------------------- /docs/css/intro.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | *, *:after, *:before { 3 | box-sizing: border-box; } 4 | 5 | body { 6 | position: relative; } 7 | 8 | .yellow-box { 9 | width: 100px; 10 | height: 100px; 11 | background-color: #fe8; 12 | pointer-events: none; } 13 | 14 | .green-box { 15 | margin-top: 65px; 16 | margin-left: 100px; 17 | width: 200px; 18 | height: 50px; 19 | background-color: #4e9; } 20 | .no-green .green-box { 21 | display: none; } 22 | 23 | .scroll-box { 24 | height: 150px; 25 | border: 10px solid #eee; 26 | background: #fbfbfb; 27 | overflow: auto; 28 | position: relative; } 29 | 30 | .scroll-content { 31 | height: 2000px; 32 | width: 2000px; 33 | padding: 910px 809px; } 34 | 35 | pre.pre-with-output { 36 | margin: 0; 37 | width: 50%; 38 | float: left; } 39 | pre.pre-with-output code mark { 40 | background: #b8daff; 41 | color: #000; } 42 | 43 | p, h2, h3 { 44 | clear: both; } 45 | 46 | output { 47 | display: block; 48 | position: relative; 49 | width: 50%; 50 | float: right; 51 | margin-bottom: 15px; } 52 | output.scroll-page .scroll-box { 53 | overflow: hidden; } 54 | output.scroll-page:after { 55 | content: "↕ scroll the page ↕"; } 56 | output:after { 57 | content: "↕ scroll this area ↕"; 58 | position: absolute; 59 | bottom: 25px; 60 | width: 100%; 61 | text-align: center; 62 | font-size: 16px; 63 | font-variant: small-caps; 64 | color: #777; 65 | opacity: 1; 66 | -webkit-transition: opacity 0.2s; 67 | transition: opacity 0.2s; } 68 | output.scrolled:after { 69 | opacity: 0; } 70 | output[deactivated], output[activated] { 71 | cursor: pointer; } 72 | output[deactivated] .scroll-box, output[activated] .scroll-box { 73 | pointer-events: none; } 74 | output[deactivated]:after, output[activated]:after { 75 | position: absolute; 76 | top: 0; 77 | left: 0; 78 | right: 0; 79 | bottom: 0; 80 | opacity: 1; 81 | content: "Click To Show"; 82 | background-color: #AAA; 83 | border-left: 10px solid #EEE; 84 | color: white; 85 | font-size: 24px; 86 | font-variant: normal; 87 | padding-top: 80px; } 88 | output[activated]:after { 89 | content: "Click To Hide"; } 90 | output[activated].visible-enabled:after { 91 | height: 35px; 92 | padding-top: 5px; } 93 | 94 | .attachment-mark, .tether-marker-dot { 95 | position: relative; } 96 | .attachment-mark:after, .tether-marker-dot:after { 97 | content: "A"; 98 | width: 10px; 99 | height: 10px; 100 | background-color: red; 101 | display: inline-block; 102 | line-height: 10px; 103 | font-size: 9px; 104 | color: white; 105 | text-align: center; 106 | position: absolute; } 107 | 108 | span.attachment-mark:after, span.tether-marker-dot:after { 109 | position: relative; 110 | top: -1px; 111 | margin-right: 1px; } 112 | 113 | .tether-marker-dot { 114 | position: absolute; } 115 | .tether-marker-dot:after { 116 | top: -5px; 117 | left: -5px; } 118 | 119 | .tether-target-marker { 120 | position: absolute; } 121 | div.tether-target-attached-left .tether-target-marker { 122 | left: 0; } 123 | div.tether-target-attached-top .tether-target-marker { 124 | top: 0; } 125 | div.tether-target-attached-bottom .tether-target-marker { 126 | bottom: 0; } 127 | div.tether-target-attached-right .tether-target-marker { 128 | right: 0; } 129 | div.tether-target-attached-center .tether-target-marker { 130 | left: 50%; } 131 | 132 | .tether-element-marker { 133 | position: absolute; } 134 | div.tether-element-attached-left .tether-element-marker { 135 | left: 0; } 136 | div.tether-element-attached-top .tether-element-marker { 137 | top: 0; } 138 | div.tether-element-attached-bottom .tether-element-marker { 139 | bottom: 0; } 140 | div.tether-element-attached-right .tether-element-marker { 141 | right: 0; } 142 | div.tether-element-attached-center .tether-element-marker { 143 | left: 50%; } 144 | 145 | .tether-element-attached-middle .tether-element-marker { 146 | top: 50px; } 147 | 148 | .tether-target-attached-middle .tether-target-marker { 149 | top: 25px; } 150 | 151 | .tether-element { 152 | position: relative; } 153 | .tether-element.tether-pinned-left { 154 | box-shadow: inset 2px 0 0 0 red; } 155 | .tether-element.tether-pinned-right { 156 | box-shadow: inset -2px 0 0 0 red; } 157 | .tether-element.tether-pinned-top { 158 | box-shadow: inset 0 2px 0 0 red; } 159 | .tether-element.tether-pinned-bottom { 160 | box-shadow: inset 0 -2px 0 0 red; } 161 | 162 | .tether-target { 163 | position: relative; } 164 | 165 | .tether-element.tether-out-of-bounds[data-example="hide"] { 166 | display: none; } 167 | 168 | [data-example^="optimizer"].lang-javascript { 169 | /* This should just be a `code` selector, but sass doesn't allow that with & */ 170 | min-height: 220px; } 171 | 172 | [data-example^="optimizer"].tether-element:before { 173 | margin-top: 26px; 174 | display: block; 175 | text-align: center; 176 | content: "I'm in the body"; 177 | line-height: 1.2; 178 | font-size: 15px; 179 | padding: 4px; 180 | color: #666; } 181 | 182 | [data-example^="optimizer"] .scroll-box .tether-element:before { 183 | content: "I'm in my scroll parent!"; } 184 | 185 | .tether-element[data-example="scroll-visible"] { 186 | height: 30px; } 187 | .tether-element[data-example="scroll-visible"] .tether-marker-dot { 188 | display: none; } 189 | 190 | .hs-doc-content h2.projects-header { 191 | text-align: center; 192 | font-weight: 300; } 193 | 194 | .projects-paragraph { 195 | text-align: center; } 196 | .projects-paragraph a { 197 | display: inline-block; 198 | vertical-align: middle; 199 | *vertical-align: auto; 200 | *zoom: 1; 201 | *display: inline; 202 | text-align: center; 203 | margin-right: 30px; 204 | color: inherit; } 205 | .projects-paragraph a span { 206 | display: inline-block; 207 | vertical-align: middle; 208 | *vertical-align: auto; 209 | *zoom: 1; 210 | *display: inline; 211 | margin-bottom: 20px; 212 | font-size: 20px; 213 | color: inherit; 214 | font-weight: 300; } 215 | .projects-paragraph a img { 216 | display: block; 217 | max-width: 100%; 218 | width: 100px; } 219 | -------------------------------------------------------------------------------- /docs/js/intro.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var OUTPUT_HTML, SETUP_JS, activate, deactivate, getOutput, init, run, setupBlock, tethers, uniqueId; 3 | 4 | uniqueId = (function() { 5 | var id = 0; 6 | return function(){ return ++id }; 7 | })(); 8 | 9 | SETUP_JS = "yellowBox = $('.yellow-box', $output);\ngreenBox = $('.green-box', $output);\nscrollBox = $('.scroll-box', $output);"; 10 | 11 | OUTPUT_HTML = function(key) { 12 | return "
\n
\n
\n
\n
\n
"; 13 | }; 14 | 15 | tethers = {}; 16 | 17 | getOutput = function($block) { 18 | var key; 19 | key = $block.data('example'); 20 | if (key && typeof key === 'string') { 21 | return $("output[data-example='" + key + "']"); 22 | } else { 23 | return $block.parents('pre').nextAll('output').first(); 24 | } 25 | }; 26 | 27 | run = function(key) { 28 | var $block, $output, code; 29 | if (typeof key === 'string') { 30 | $block = $("code[data-example='" + key + "']"); 31 | } else { 32 | $block = key; 33 | } 34 | key = $block.attr('data-example'); 35 | $output = getOutput($block); 36 | code = $block.text(); 37 | code = SETUP_JS + code; 38 | window.$output = $output; 39 | return tethers[key] = eval(code); 40 | }; 41 | 42 | setupBlock = function($block) { 43 | var $output, $scrollBox, $scrollContent, key; 44 | key = $block.data('example'); 45 | $output = getOutput($block); 46 | if (!key) { 47 | key = uniqueId(); 48 | $block.attr('data-example', key); 49 | $output.attr('data-example', key); 50 | $output.find('.tether-element').attr('data-example', key); 51 | } 52 | $output.html(OUTPUT_HTML(key)); 53 | $scrollBox = $output.find('.scroll-box'); 54 | $scrollContent = $scrollBox.find('.scroll-content'); 55 | $scrollBox.scrollTop(parseInt($scrollContent.css('height')) / 2 - $scrollBox.height() / 2); 56 | $scrollBox.scrollLeft(parseInt($scrollContent.css('width')) / 2 - $scrollBox.width() / 2); 57 | setTimeout(function() { 58 | return $scrollBox.on('scroll', function() { 59 | return $output.addClass('scrolled'); 60 | }); 61 | }); 62 | $scrollBox.css('height', "" + ($block.parent().outerHeight()) + "px"); 63 | if ($output.attr('deactivated') == null) { 64 | return run($block); 65 | } 66 | }; 67 | 68 | $(document.body).on('click', function(e) { 69 | if ($(e.target).is('output[deactivated]')) { 70 | activate($(e.target)); 71 | return false; 72 | } else if ($(e.target).is('output[activated]')) { 73 | deactivate($(e.target)); 74 | return false; 75 | } 76 | }); 77 | 78 | activate = function($output) { 79 | var $block, key; 80 | $block = $output.prev().find('code'); 81 | run($block); 82 | $output.find('.tether-element').show(); 83 | key = $output.data('example'); 84 | $(tethers[key].element).show(); 85 | tethers[key].enable(); 86 | $output.removeAttr('deactivated'); 87 | return $output.attr('activated', true); 88 | }; 89 | 90 | deactivate = function($output) { 91 | var $block, $el, key; 92 | $block = $output.prev().find('code'); 93 | key = $output.data('example'); 94 | tethers[key].disable(); 95 | $el = $(tethers[key].element); 96 | $el.detach(); 97 | $output.find('.scroll-content').append($el); 98 | $el.hide(); 99 | $output.removeAttr('activated'); 100 | return $output.attr('deactivated', true); 101 | }; 102 | 103 | init = function() { 104 | var $blocks, block, _i, _len, _results; 105 | $blocks = $('code[data-example]'); 106 | _results = []; 107 | for (_i = 0, _len = $blocks.length; _i < _len; _i++) { 108 | block = $blocks[_i]; 109 | _results.push(setupBlock($(block))); 110 | } 111 | return _results; 112 | }; 113 | 114 | window.EXECUTR_OPTIONS = { 115 | codeSelector: 'code[executable]' 116 | }; 117 | 118 | $(init); 119 | 120 | }).call(this); 121 | -------------------------------------------------------------------------------- /docs/sass/intro.sass: -------------------------------------------------------------------------------- 1 | $scrollableArea: 2000px 2 | $exampleWidth: 400px 3 | $exampleHeight: 180px 4 | 5 | @mixin inline-block 6 | display: inline-block 7 | vertical-align: middle 8 | *vertical-align: auto 9 | *zoom: 1 10 | *display: inline 11 | 12 | *, *:after, *:before 13 | box-sizing: border-box 14 | 15 | body 16 | position: relative 17 | 18 | .yellow-box 19 | width: 100px 20 | height: 100px 21 | background-color: #fe8 22 | pointer-events: none 23 | 24 | .green-box 25 | margin-top: ($exampleHeight - 50px) / 2 26 | margin-left: ($exampleWidth - 200px) / 2 27 | width: 200px 28 | height: 50px 29 | background-color: #4e9 30 | 31 | .no-green & 32 | display: none 33 | 34 | .scroll-box 35 | height: 150px 36 | border: 10px solid #eee 37 | background: #fbfbfb 38 | overflow: auto 39 | position: relative 40 | 41 | .scroll-content 42 | height: $scrollableArea 43 | width: $scrollableArea 44 | padding: ($scrollableArea - $exampleHeight)/2 ($scrollableArea - $exampleWidth)/2 + 9 45 | 46 | pre.pre-with-output 47 | margin: 0 48 | width: 50% 49 | float: left 50 | 51 | code mark 52 | background: #b8daff 53 | color: #000 54 | 55 | p, h2, h3 56 | clear: both 57 | 58 | output 59 | display: block 60 | position: relative 61 | width: 50% 62 | float: right 63 | margin-bottom: 15px 64 | 65 | &.scroll-page 66 | .scroll-box 67 | overflow: hidden 68 | 69 | &:after 70 | content: "↕ scroll the page ↕" 71 | 72 | &:after 73 | content: "↕ scroll this area ↕" 74 | position: absolute 75 | bottom: 25px 76 | width: 100% 77 | text-align: center 78 | font-size: 16px 79 | font-variant: small-caps 80 | color: #777 81 | opacity: 1 82 | transition: opacity 0.2s 83 | 84 | &.scrolled:after 85 | opacity: 0 86 | 87 | &[deactivated], &[activated] 88 | .scroll-box 89 | pointer-events: none 90 | 91 | cursor: pointer 92 | 93 | &:after 94 | position: absolute 95 | top: 0 96 | left: 0 97 | right: 0 98 | bottom: 0 99 | opacity: 1 100 | content: "Click To Show" 101 | background-color: #AAA 102 | border-left: 10px solid #EEE 103 | color: white 104 | font-size: 24px 105 | font-variant: normal 106 | padding-top: 80px 107 | 108 | &[activated] 109 | &:after 110 | content: "Click To Hide" 111 | 112 | &.visible-enabled 113 | &:after 114 | height: 35px 115 | padding-top: 5px 116 | 117 | .attachment-mark 118 | position: relative 119 | 120 | &:after 121 | content: "A" 122 | width: 10px 123 | height: 10px 124 | background-color: red 125 | display: inline-block 126 | 127 | line-height: 10px 128 | font-size: 9px 129 | color: white 130 | text-align: center 131 | 132 | position: absolute 133 | 134 | span.attachment-mark 135 | &:after 136 | position: relative 137 | top: -1px 138 | margin-right: 1px 139 | 140 | .tether-marker-dot 141 | @extend .attachment-mark 142 | 143 | position: absolute 144 | 145 | &:after 146 | top: -5px 147 | left: -5px 148 | 149 | @each $type in target, element 150 | .tether-#{ $type }-marker 151 | position: absolute 152 | 153 | @each $side in left, top, bottom, right 154 | div.tether-#{ $type }-attached-#{ $side } & 155 | #{ $side }: 0 156 | 157 | div.tether-#{ $type }-attached-center & 158 | left: 50% 159 | 160 | .tether-element-attached-middle .tether-element-marker 161 | top: 50px 162 | 163 | .tether-target-attached-middle .tether-target-marker 164 | top: 25px 165 | 166 | .tether-element 167 | position: relative 168 | 169 | &.tether-pinned-left 170 | box-shadow: inset 2px 0 0 0 red 171 | &.tether-pinned-right 172 | box-shadow: inset -2px 0 0 0 red 173 | &.tether-pinned-top 174 | box-shadow: inset 0 2px 0 0 red 175 | &.tether-pinned-bottom 176 | box-shadow: inset 0 -2px 0 0 red 177 | 178 | .tether-target 179 | position: relative 180 | 181 | .tether-element.tether-out-of-bounds[data-example="hide"] 182 | display: none 183 | 184 | [data-example^="optimizer"] 185 | &.lang-javascript 186 | /* This should just be a `code` selector, but sass doesn't allow that with & */ 187 | min-height: 220px 188 | 189 | &.tether-element 190 | 191 | &:before 192 | margin-top: 26px 193 | display: block 194 | text-align: center 195 | content: "I'm in the body" 196 | line-height: 1.2 197 | font-size: 15px 198 | padding: 4px 199 | color: #666 200 | 201 | .scroll-box .tether-element:before 202 | content: "I'm in my scroll parent!" 203 | 204 | .tether-element[data-example="scroll-visible"] 205 | height: 30px 206 | 207 | .tether-marker-dot 208 | display: none 209 | 210 | .hs-doc-content h2.projects-header 211 | text-align: center 212 | font-weight: 300 213 | 214 | .projects-paragraph 215 | text-align: center 216 | 217 | a 218 | +inline-block 219 | text-align: center 220 | margin-right: 30px 221 | color: inherit 222 | 223 | span 224 | +inline-block 225 | margin-bottom: 20px 226 | font-size: 20px 227 | color: inherit 228 | font-weight: 300 229 | 230 | img 231 | display: block 232 | max-width: 100% 233 | width: 100px 234 | -------------------------------------------------------------------------------- /docs/welcome/browser-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Drop – Browser Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |

59 |

60 |

61 |

62 |

63 |

64 |
65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/welcome/css/browser-demo.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | overflow: hidden; 4 | font-family: "proxima-nova", sans-serif; } 5 | 6 | .tether.tether-theme-arrows-dark .tether-content { 7 | -webkit-filter: none; 8 | filter: none; 9 | background: #000; } 10 | .tether.tether-theme-arrows-dark .tether-content ul { 11 | color: #fff; 12 | list-style: none; 13 | padding: 0; 14 | margin: 0; } 15 | 16 | .tether.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content:before { 17 | border-right-color: #000; } 18 | 19 | .browser-demo { 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | bottom: 0; 24 | right: 0; } 25 | .browser-demo *, .browser-demo *:after, .browser-demo *:before { 26 | box-sizing: border-box; } 27 | .browser-demo .top { 28 | position: absolute; 29 | height: 60px; 30 | padding: 20px; 31 | line-height: 40px; 32 | width: 100%; 33 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); } 34 | .browser-demo .bottom { 35 | position: absolute; 36 | top: 60px; 37 | bottom: 0; 38 | width: 100%; } 39 | .browser-demo .bottom .left { 40 | border-right: 1px solid rgba(0, 0, 0, 0.1); 41 | position: absolute; 42 | width: 30%; 43 | height: 100%; 44 | overflow: auto; } 45 | .browser-demo .bottom .left .item { 46 | height: 64px; 47 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 48 | cursor: pointer; } 49 | .browser-demo .bottom .left .item:hover, .browser-demo .bottom .left .item.tether-open { 50 | background: rgba(0, 0, 0, 0.1); 51 | border-bottom: 1px solid transparent; } 52 | .browser-demo .bottom .left .item:last-child { 53 | border-bottom: 0; } 54 | .browser-demo .bottom .right { 55 | position: absolute; 56 | width: 70%; 57 | right: 0; 58 | height: 100%; 59 | padding: 20px; } 60 | .browser-demo .title { 61 | display: inline-block; 62 | vertical-align: middle; 63 | *vertical-align: auto; 64 | *zoom: 1; 65 | *display: inline; 66 | background: rgba(0, 0, 0, 0.1); 67 | width: 150px; 68 | height: 15px; 69 | margin-bottom: 20px; } 70 | .browser-demo .word { 71 | display: inline-block; 72 | vertical-align: middle; 73 | *vertical-align: auto; 74 | *zoom: 1; 75 | *display: inline; 76 | background: rgba(0, 0, 0, 0.1); 77 | width: 50px; 78 | height: 8px; 79 | margin-right: 5px; 80 | margin-bottom: 5px; } 81 | .browser-demo .word:nth-last-child(4n+1) { 82 | width: 73px; } 83 | .browser-demo .word:nth-last-child(10n+1) { 84 | width: 14px; } 85 | .browser-demo .word:nth-last-child(9n+1) { 86 | width: 80px; } 87 | -------------------------------------------------------------------------------- /docs/welcome/css/prism.css: -------------------------------------------------------------------------------- 1 | /* Prism.js */ 2 | code[class*="language-"], pre[class*="language-"] {color: black; font-family: Consolas, Monaco, 'Andale Mono', monospace; direction: ltr; text-align: left; white-space: pre; word-spacing: normal; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*="language-"] {padding: 1em; margin: .5em 0; overflow: auto; font-size: 14px; } :not(pre) > code[class*="language-"], pre[class*="language-"] {background: rgba(0, 0, 0, .05); } /* Inline code */ :not(pre) > code[class*="language-"] {padding: .1em; border-radius: .3em; } .token.comment, .token.prolog, .token.doctype, .token.cdata {color: slategray; } .token.punctuation {color: #999; } .namespace {opacity: .7; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol {color: #905; } .token.selector, .token.attr-name, .token.string, .token.builtin {color: #690; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable {color: #a67f59; } .token.atrule, .token.attr-value, .token.keyword {color: #07a; } .token.regex, .token.important {color: #e90; } .token.important {font-weight: bold; } .token.entity {cursor: help; } -------------------------------------------------------------------------------- /docs/welcome/css/welcome.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; } 3 | 4 | body { 5 | margin: 0; 6 | font-family: "proxima-nova", "Helvetica Neue", sans-serif; } 7 | 8 | .button { 9 | display: inline-block; 10 | border: 2px solid #333; 11 | color: #333; 12 | padding: 1em 1.25em; 13 | font-weight: 500; 14 | text-transform: uppercase; 15 | letter-spacing: 3px; 16 | text-decoration: none; 17 | cursor: pointer; 18 | width: 140px; 19 | font-size: .8em; 20 | line-height: 1.3em; 21 | text-align: center; } 22 | 23 | .tether-element.tether-theme-arrows-dark .tether-content { 24 | padding: 1em; 25 | font-size: 1.1em; } 26 | .tether-element.tether-theme-arrows-dark .tether-content .button { 27 | border-color: #fff; 28 | color: #fff; 29 | width: 170px; 30 | pointer-events: all; } 31 | 32 | .mobile-copy { 33 | display: none; } 34 | @media (max-width: 568px) { 35 | .mobile-copy { 36 | display: block; } } 37 | 38 | .button.dark { 39 | background: #333; 40 | color: #fff; } 41 | 42 | .hero-wrap { 43 | height: 100%; 44 | overflow: hidden; } 45 | 46 | table.showcase { 47 | height: 100%; 48 | width: 100%; 49 | position: relative; } 50 | table.showcase:after { 51 | content: ""; 52 | display: block; 53 | position: absolute; 54 | left: 0; 55 | right: 0; 56 | bottom: 20px; 57 | margin: auto; 58 | height: 0; 59 | width: 0; 60 | border-width: 18px; 61 | border-style: solid; 62 | border-color: transparent; 63 | border-top-color: rgba(0, 0, 0, 0.2); } 64 | table.showcase.no-next-arrow:after { 65 | display: none; } 66 | table.showcase .showcase-inner { 67 | margin: 40px auto 60px; 68 | padding: 10px; } 69 | table.showcase .showcase-inner h1 { 70 | font-size: 50px; 71 | text-align: center; 72 | font-weight: 300; } 73 | @media (max-width: 567px) { 74 | table.showcase .showcase-inner h1 { 75 | font-size: 40px; } } 76 | table.showcase .showcase-inner h2 { 77 | font-size: 24px; 78 | text-align: center; 79 | font-weight: 300; 80 | margin: 1em 0 1em; } 81 | @media (max-width: 567px) { 82 | table.showcase .showcase-inner h2 { 83 | font-size: 14px; } } 84 | table.showcase .showcase-inner p { 85 | text-align: center; } 86 | table.showcase.hero { 87 | text-align: center; } 88 | table.showcase.hero .tether-target-demo { 89 | display: inline-block; 90 | vertical-align: middle; 91 | *vertical-align: auto; 92 | *zoom: 1; 93 | *display: inline; 94 | border: 2px dotted #000; 95 | margin: 5rem auto; 96 | padding: 5rem; } 97 | @media (max-width: 567px) { 98 | table.showcase.hero .tether-target-demo { 99 | padding: 1rem; } } 100 | table.showcase.share { 101 | background: #f3f3f3; } 102 | table.showcase.projects-showcase .showcase-inner .projects-list { 103 | width: 80%; 104 | max-width: 1200px; 105 | margin: 0 auto; } 106 | table.showcase.projects-showcase .showcase-inner .projects-list .project { 107 | color: inherit; 108 | text-decoration: none; 109 | position: relative; 110 | width: 50%; 111 | float: left; 112 | text-align: center; 113 | margin-bottom: 2rem; } 114 | table.showcase.projects-showcase .showcase-inner .projects-list .project:nth-child(odd) { 115 | clear: left; } 116 | table.showcase.projects-showcase .showcase-inner .projects-list .os-icon { 117 | width: 8rem; 118 | height: 8rem; 119 | margin-bottom: 1rem; 120 | background-size: 100%; } 121 | table.showcase.projects-showcase .showcase-inner .projects-list h1 { 122 | font-size: 2.5rem; } 123 | table.showcase.projects-showcase .showcase-inner .projects-list p { 124 | font-size: 1.3rem; } 125 | table.showcase.browser-demo { 126 | background-image: -webkit-linear-gradient(top left, #723362 0%, #9d223c 100%); 127 | background-image: linear-gradient(top left, #723362 0%, #9d223c 100%); 128 | background-color: #9d223c; 129 | position: absolute; 130 | top: 100%; } 131 | table.showcase.browser-demo.fixed { 132 | position: fixed; 133 | top: 0; 134 | bottom: 0; 135 | left: 0; 136 | right: 0; 137 | z-index: 1; } 138 | table.showcase.browser-demo.fixed .browser-demo-inner { 139 | -webkit-transition: width 2s ease-in-out, height 2s ease-in-out; 140 | transition: width 2s ease-in-out, height 2s ease-in-out; } 141 | table.showcase.browser-demo.fixed[data-section="what"] { 142 | box-shadow: 0 0 0 0; } 143 | table.showcase.browser-demo.fixed[data-section="why"] .browser-demo-inner { 144 | width: 70%; } 145 | table.showcase.browser-demo.fixed[data-section="outro"] .showcase-inner { 146 | pointer-events: all; } 147 | table.showcase.browser-demo .showcase-inner { 148 | pointer-events: none; 149 | position: absolute; 150 | left: 10%; 151 | right: 40%; 152 | top: 220px; 153 | bottom: 120px; 154 | margin: 0; 155 | padding: 0; } 156 | @media (max-width: 567px) { 157 | table.showcase.browser-demo .showcase-inner { 158 | bottom: 90px; 159 | top: 180px; } } 160 | table.showcase.browser-demo .browser-demo-inner { 161 | height: 100%; 162 | width: 100%; } 163 | table.showcase.browser-demo .section-copy { 164 | -webkit-transition: opacity 0.5s ease-in-out, top 0.5s ease-in-out; 165 | transition: opacity 0.5s ease-in-out, top 0.5s ease-in-out; 166 | opacity: 0; 167 | position: absolute; 168 | top: 0; 169 | position: absolute; 170 | height: 200px; 171 | color: #fff; 172 | text-align: center; 173 | width: 100%; } 174 | table.showcase.browser-demo .section-copy.active { 175 | opacity: 1; 176 | top: -150px; } 177 | @media (max-width: 567px) { 178 | table.showcase.browser-demo .section-copy.active { 179 | top: -130px; } } 180 | table.showcase.browser-demo .section-copy h2 { 181 | font-size: 40px; 182 | font-weight: bold; 183 | line-height: 1; 184 | margin: 25px 0 15px; } 185 | @media (max-width: 567px) { 186 | table.showcase.browser-demo .section-copy h2 { 187 | font-size: 30px; } } 188 | table.showcase.browser-demo .browser-window { 189 | border-radius: 4px; 190 | background: #fff; 191 | position: relative; 192 | height: 100%; 193 | width: 100%; 194 | max-width: 1200px; 195 | margin: 0 auto; } 196 | table.showcase.browser-demo .browser-window .browser-titlebar { 197 | position: absolute; 198 | top: 0; 199 | left: 0; 200 | right: 0; 201 | border-bottom: 1px solid #eee; 202 | height: 55px; } 203 | table.showcase.browser-demo .browser-window .browser-titlebar .browser-dots { 204 | padding: 16px; } 205 | table.showcase.browser-demo .browser-window .browser-titlebar .browser-dots b { 206 | display: inline-block; 207 | vertical-align: middle; 208 | *vertical-align: auto; 209 | *zoom: 1; 210 | *display: inline; 211 | border-radius: 50%; 212 | width: 10px; 213 | height: 10px; 214 | margin-right: 7px; 215 | background: rgba(0, 0, 0, 0.1); } 216 | table.showcase.browser-demo .browser-window .browser-frame { 217 | position: absolute; 218 | top: 55px; 219 | left: 0; 220 | right: 0; 221 | bottom: 0; } 222 | table.showcase.browser-demo .browser-window .browser-frame iframe { 223 | border-radius: 0 0 4px 4px; 224 | border: 0; 225 | width: 100%; 226 | height: 100%; } 227 | table.showcase.browser-demo-section .section-scroll-copy { 228 | position: relative; 229 | z-index: 10; 230 | color: #fff; 231 | width: 100%; 232 | font-size: 22px; } 233 | table.showcase.browser-demo-section .section-scroll-copy .section-scroll-copy-inner { 234 | position: absolute; 235 | z-index: 10; 236 | color: #fff; 237 | right: 10%; 238 | width: 23%; } 239 | table.showcase.browser-demo-section .section-scroll-copy .section-scroll-copy-inner a { 240 | color: inherit; } 241 | table.showcase.browser-demo-section .section-scroll-copy .section-scroll-copy-inner .example-paragraph { 242 | border-radius: 4px; 243 | background: #000; 244 | padding: 1rem; } 245 | 246 | .browser-content { 247 | display: none; } 248 | -------------------------------------------------------------------------------- /docs/welcome/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tether – Marrying elements for life 6 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 43 | 44 |
31 |
32 |
33 |

Tether

34 |
35 |

Marrying elements for life

36 |

37 | ★ On Github 38 |

39 |
40 |
41 |
42 |
45 |
46 | 47 |
48 | 49 | 50 | 51 | 77 | 78 |
52 |
53 |
54 |

What is Tether?

55 |
56 |
57 |

How Tether works.

58 |
59 |
60 |

Tether is powerful.

61 |
62 |
63 |

Play with Tether

64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
79 | 80 | 81 | 82 | 92 | 93 |
83 |
84 |
85 |

Tether is a low-level UI library that can be used to position any element on a page next to any other element.

86 |

It can be used for dropdown menus, tooltips, popovers, tours, help information, scroll guides, autocompletes, etc. The possibilities are 87 | endless.

88 |

In this example we're showing an action menu tethered to a list item.

89 |
90 |
91 |
94 | 95 | 96 | 97 | 107 | 108 |
98 |
99 |
100 |

Tether works by creating an absolutely positioned element and meticulously tracking the movements of a target which you specify.

101 |

The target and element can be tethered together in a variety of different ways.

102 |

Notice how the tethered element stays tethered to its target list item even as the left pane is scrolled up 103 | and down.

104 |
105 |
106 |
109 | 110 | 111 | 112 | 126 | 127 |
113 |
114 |
115 |

Tether can keep your element positioned properly even in some tough situations.

116 |

Tether handles all of the common pain points:

117 |
    118 |
  • Automatically detect collisions with the edge of the page or edge of the scrollParent
  • 119 |
  • Automatically reposition on browser resize, scroll, and other events,
  • 120 |
  • Constrain the position to any bounding box,
  • 121 |
122 |

...and a lot more.

123 |
124 |
125 |
128 | 129 | 130 | 131 | 140 | 141 |
132 |
133 |
134 |

Interact with this demo.

135 |

 

136 |

To learn more, check out our documentation.

137 |
138 |
139 |
142 | 143 | 144 | 145 | 146 | 147 |
148 | 149 |
150 | 151 | 152 | 153 | 181 | 182 |
154 | 180 |
183 | 184 | 185 | 186 | 240 | 241 |
187 |
188 |

Share

189 |

Help us spread the word.

190 | 191 | 210 | 239 |
242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /docs/welcome/js/drop.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Evented, MIRROR_ATTACH, addClass, allDrops, clickEvent, createContext, extend, hasClass, removeClass, sortAttach, touchDevice, _ref, 3 | __hasProp = {}.hasOwnProperty, 4 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 5 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 6 | 7 | _ref = Tether.Utils, extend = _ref.extend, addClass = _ref.addClass, removeClass = _ref.removeClass, hasClass = _ref.hasClass, Evented = _ref.Evented; 8 | 9 | touchDevice = 'ontouchstart' in document.documentElement; 10 | 11 | clickEvent = touchDevice ? 'touchstart' : 'click'; 12 | 13 | sortAttach = function(str) { 14 | var first, second, _ref1, _ref2; 15 | _ref1 = str.split(' '), first = _ref1[0], second = _ref1[1]; 16 | if (first === 'left' || first === 'right') { 17 | _ref2 = [second, first], first = _ref2[0], second = _ref2[1]; 18 | } 19 | return [first, second].join(' '); 20 | }; 21 | 22 | MIRROR_ATTACH = { 23 | left: 'right', 24 | right: 'left', 25 | top: 'bottom', 26 | bottom: 'top', 27 | middle: 'middle', 28 | center: 'center' 29 | }; 30 | 31 | allDrops = {}; 32 | 33 | createContext = function(options) { 34 | var DropInstance, defaultOptions, drop, _name; 35 | if (options == null) { 36 | options = {}; 37 | } 38 | drop = function() { 39 | return (function(func, args, ctor) { 40 | ctor.prototype = func.prototype; 41 | var child = new ctor, result = func.apply(child, args); 42 | return Object(result) === result ? result : child; 43 | })(DropInstance, arguments, function(){}); 44 | }; 45 | extend(drop, { 46 | createContext: createContext, 47 | drops: [], 48 | defaults: {} 49 | }); 50 | defaultOptions = { 51 | classPrefix: 'drop', 52 | defaults: { 53 | attach: 'bottom left', 54 | openOn: 'click', 55 | constrainToScrollParent: true, 56 | constrainToWindow: true, 57 | classes: '', 58 | tetherOptions: {} 59 | } 60 | }; 61 | extend(drop, defaultOptions, options); 62 | extend(drop.defaults, defaultOptions.defaults, options.defaults); 63 | if (allDrops[_name = drop.classPrefix] == null) { 64 | allDrops[_name] = []; 65 | } 66 | drop.updateBodyClasses = function() { 67 | var anyOpen, _drop, _i, _len, _ref1; 68 | anyOpen = false; 69 | _ref1 = allDrops[drop.classPrefix]; 70 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 71 | _drop = _ref1[_i]; 72 | if (!(_drop.isOpened())) { 73 | continue; 74 | } 75 | anyOpen = true; 76 | break; 77 | } 78 | if (anyOpen) { 79 | return addClass(document.body, "" + drop.classPrefix + "-open"); 80 | } else { 81 | return removeClass(document.body, "" + drop.classPrefix + "-open"); 82 | } 83 | }; 84 | DropInstance = (function(_super) { 85 | __extends(DropInstance, _super); 86 | 87 | function DropInstance(options) { 88 | this.options = options; 89 | this.options = extend({}, drop.defaults, this.options); 90 | this.target = this.options.target; 91 | if (this.target == null) { 92 | throw new Error('Drop Error: You must provide a target.'); 93 | } 94 | drop.drops.push(this); 95 | allDrops[drop.classPrefix].push(this); 96 | this.setupElements(); 97 | this.setupEvents(); 98 | this.setupTether(); 99 | } 100 | 101 | DropInstance.prototype.setupElements = function() { 102 | this.drop = document.createElement('div'); 103 | addClass(this.drop, drop.classPrefix); 104 | if (this.options.classes) { 105 | addClass(this.drop, this.options.classes); 106 | } 107 | this.dropContent = document.createElement('div'); 108 | addClass(this.dropContent, "" + drop.classPrefix + "-content"); 109 | if (typeof this.options.content === 'object') { 110 | this.dropContent.appendChild(this.options.content); 111 | } else { 112 | this.dropContent.innerHTML = this.options.content; 113 | } 114 | return this.drop.appendChild(this.dropContent); 115 | }; 116 | 117 | DropInstance.prototype.setupTether = function() { 118 | var constraints, dropAttach; 119 | dropAttach = this.options.position.split(' '); 120 | dropAttach[0] = MIRROR_ATTACH[dropAttach[0]]; 121 | dropAttach = dropAttach.join(' '); 122 | constraints = []; 123 | if (this.options.constrainToScrollParent) { 124 | constraints.push({ 125 | to: 'scrollParent', 126 | pin: 'top, bottom', 127 | attachment: 'together none' 128 | }); 129 | } 130 | if (this.options.constrainToWindow !== false) { 131 | constraints.push({ 132 | to: 'window', 133 | pin: true, 134 | attachment: 'together' 135 | }); 136 | } 137 | constraints.push({ 138 | to: 'scrollParent' 139 | }); 140 | options = { 141 | element: this.drop, 142 | target: this.target, 143 | attachment: sortAttach(dropAttach), 144 | targetAttachment: sortAttach(this.options.position), 145 | classPrefix: drop.classPrefix, 146 | offset: '0 0', 147 | targetOffset: '0 0', 148 | enabled: false, 149 | constraints: constraints 150 | }; 151 | if (this.options.tether !== false) { 152 | return this.tether = new Tether(extend({}, options, this.options.tether)); 153 | } 154 | }; 155 | 156 | DropInstance.prototype.setupEvents = function() { 157 | var events, 158 | _this = this; 159 | if (!this.options.openOn) { 160 | return; 161 | } 162 | events = this.options.openOn.split(' '); 163 | if (__indexOf.call(events, 'click') >= 0) { 164 | this.target.addEventListener(clickEvent, function() { 165 | return _this.toggle(); 166 | }); 167 | document.addEventListener(clickEvent, function(event) { 168 | if (!_this.isOpened()) { 169 | return; 170 | } 171 | if (event.target === _this.drop || _this.drop.contains(event.target)) { 172 | return; 173 | } 174 | if (event.target === _this.target || _this.target.contains(event.target)) { 175 | return; 176 | } 177 | return _this.close(); 178 | }); 179 | } 180 | if (__indexOf.call(events, 'hover') >= 0) { 181 | this.target.addEventListener('mouseover', function() { 182 | return _this.open(); 183 | }); 184 | return this.target.addEventListener('mouseout', function() { 185 | return _this.close(); 186 | }); 187 | } 188 | }; 189 | 190 | DropInstance.prototype.isOpened = function() { 191 | return hasClass(this.drop, "" + drop.classPrefix + "-open"); 192 | }; 193 | 194 | DropInstance.prototype.toggle = function() { 195 | if (this.isOpened()) { 196 | return this.close(); 197 | } else { 198 | return this.open(); 199 | } 200 | }; 201 | 202 | DropInstance.prototype.open = function() { 203 | var _ref1; 204 | if (!this.drop.parentNode) { 205 | document.body.appendChild(this.drop); 206 | } 207 | addClass(this.target, "" + drop.classPrefix + "-open"); 208 | addClass(this.drop, "" + drop.classPrefix + "-open"); 209 | if ((_ref1 = this.tether) != null) { 210 | _ref1.enable(); 211 | } 212 | this.trigger('open'); 213 | return drop.updateBodyClasses(); 214 | }; 215 | 216 | DropInstance.prototype.close = function() { 217 | var _ref1; 218 | removeClass(this.target, "" + drop.classPrefix + "-open"); 219 | removeClass(this.drop, "" + drop.classPrefix + "-open"); 220 | this.trigger('close'); 221 | if ((_ref1 = this.tether) != null) { 222 | _ref1.disable(); 223 | } 224 | return drop.updateBodyClasses(); 225 | }; 226 | 227 | return DropInstance; 228 | 229 | })(Evented); 230 | return drop; 231 | }; 232 | 233 | window.Drop = createContext(); 234 | 235 | document.addEventListener('DOMContentLoaded', function() { 236 | return Drop.updateBodyClasses(); 237 | }); 238 | 239 | }).call(this); 240 | -------------------------------------------------------------------------------- /docs/welcome/js/log.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var ffSupport, formats, getOrderedMatches, hasMatches, isFF, isIE, isOpera, isSafari, log, makeArray, operaSupport, safariSupport, stringToArgs, _log; 3 | if (!(window.console && window.console.log)) { 4 | return; 5 | } 6 | log = function() { 7 | var args; 8 | args = []; 9 | makeArray(arguments).forEach(function(arg) { 10 | if (typeof arg === 'string') { 11 | return args = args.concat(stringToArgs(arg)); 12 | } else { 13 | return args.push(arg); 14 | } 15 | }); 16 | return _log.apply(window, args); 17 | }; 18 | _log = function() { 19 | return console.log.apply(console, makeArray(arguments)); 20 | }; 21 | makeArray = function(arrayLikeThing) { 22 | return Array.prototype.slice.call(arrayLikeThing); 23 | }; 24 | formats = [ 25 | { 26 | regex: /\*([^\*]+)\*/, 27 | replacer: function(m, p1) { 28 | return "%c" + p1 + "%c"; 29 | }, 30 | styles: function() { 31 | return ['font-style: italic', '']; 32 | } 33 | }, { 34 | regex: /\_([^\_]+)\_/, 35 | replacer: function(m, p1) { 36 | return "%c" + p1 + "%c"; 37 | }, 38 | styles: function() { 39 | return ['font-weight: bold', '']; 40 | } 41 | }, { 42 | regex: /\`([^\`]+)\`/, 43 | replacer: function(m, p1) { 44 | return "%c" + p1 + "%c"; 45 | }, 46 | styles: function() { 47 | return ['background: rgb(255, 255, 219); padding: 1px 5px; border: 1px solid rgba(0, 0, 0, 0.1)', '']; 48 | } 49 | }, { 50 | regex: /\[c\=(?:\"|\')?((?:(?!(?:\"|\')\]).)*)(?:\"|\')?\]((?:(?!\[c\]).)*)\[c\]/, 51 | replacer: function(m, p1, p2) { 52 | return "%c" + p2 + "%c"; 53 | }, 54 | styles: function(match) { 55 | return [match[1], '']; 56 | } 57 | } 58 | ]; 59 | hasMatches = function(str) { 60 | var _hasMatches; 61 | _hasMatches = false; 62 | formats.forEach(function(format) { 63 | if (format.regex.test(str)) { 64 | return _hasMatches = true; 65 | } 66 | }); 67 | return _hasMatches; 68 | }; 69 | getOrderedMatches = function(str) { 70 | var matches; 71 | matches = []; 72 | formats.forEach(function(format) { 73 | var match; 74 | match = str.match(format.regex); 75 | if (match) { 76 | return matches.push({ 77 | format: format, 78 | match: match 79 | }); 80 | } 81 | }); 82 | return matches.sort(function(a, b) { 83 | return a.match.index - b.match.index; 84 | }); 85 | }; 86 | stringToArgs = function(str) { 87 | var firstMatch, matches, styles; 88 | styles = []; 89 | while (hasMatches(str)) { 90 | matches = getOrderedMatches(str); 91 | firstMatch = matches[0]; 92 | str = str.replace(firstMatch.format.regex, firstMatch.format.replacer); 93 | styles = styles.concat(firstMatch.format.styles(firstMatch.match)); 94 | } 95 | return [str].concat(styles); 96 | }; 97 | isSafari = function() { 98 | return /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor); 99 | }; 100 | isOpera = function() { 101 | return /OPR/.test(navigator.userAgent) && /Opera/.test(navigator.vendor); 102 | }; 103 | isFF = function() { 104 | return /Firefox/.test(navigator.userAgent); 105 | }; 106 | isIE = function() { 107 | return /MSIE/.test(navigator.userAgent); 108 | }; 109 | safariSupport = function() { 110 | var m; 111 | m = navigator.userAgent.match(/AppleWebKit\/(\d+)\.(\d+)(\.|\+|\s)/); 112 | if (!m) { 113 | return false; 114 | } 115 | return 537.38 <= parseInt(m[1], 10) + (parseInt(m[2], 10) / 100); 116 | }; 117 | operaSupport = function() { 118 | var m; 119 | m = navigator.userAgent.match(/OPR\/(\d+)\./); 120 | if (!m) { 121 | return false; 122 | } 123 | return 15 <= parseInt(m[1], 10); 124 | }; 125 | ffSupport = function() { 126 | return window.console.firebug || window.console.exception; 127 | }; 128 | if (isIE() || (isFF() && !ffSupport()) || (isOpera() && !operaSupport()) || (isSafari() && !safariSupport())) { 129 | window.log = _log; 130 | } else { 131 | window.log = log; 132 | } 133 | window.log.l = _log; 134 | }).call(this); -------------------------------------------------------------------------------- /docs/welcome/js/welcome.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var init, isMobile, setupBrowserDemo, setupHero, _Drop; 3 | 4 | _Drop = Drop.createContext({ 5 | classPrefix: 'tether' 6 | }); 7 | 8 | isMobile = $(window).width() < 567; 9 | 10 | init = function() { 11 | setupHero(); 12 | return setupBrowserDemo(); 13 | }; 14 | 15 | setupHero = function() { 16 | var $target, finalDropState, frameLengthMS, frames, openAllDrops, openIndex, openNextDrop, position, positions, _i, _len; 17 | $target = $('.tether-target-demo'); 18 | positions = ['top left', 'left top', 'left middle', 'left bottom', 'bottom left', 'bottom center', 'bottom right', 'right bottom', 'right middle', 'right top', 'top right', 'top center']; 19 | if (isMobile) { 20 | positions = ['top left', 'bottom left', 'bottom right', 'top right']; 21 | } 22 | window.drops = {}; 23 | for (_i = 0, _len = positions.length; _i < _len; _i++) { 24 | position = positions[_i]; 25 | drops[position] = new _Drop({ 26 | target: $target[0], 27 | classes: 'tether-theme-arrows-dark', 28 | position: position, 29 | constrainToWindow: false, 30 | openOn: '', 31 | content: '
' 32 | }); 33 | } 34 | openIndex = 0; 35 | frames = 0; 36 | frameLengthMS = 10; 37 | openAllDrops = function() { 38 | var drop, _results; 39 | _results = []; 40 | for (position in drops) { 41 | drop = drops[position]; 42 | _results.push(drop.open()); 43 | } 44 | return _results; 45 | }; 46 | openNextDrop = function() { 47 | var drop; 48 | for (position in drops) { 49 | drop = drops[position]; 50 | drop.close(); 51 | } 52 | drops[positions[openIndex]].open(); 53 | drops[positions[(openIndex + 6) % positions.length]].open(); 54 | openIndex = (openIndex + 1) % positions.length; 55 | if (frames > 5) { 56 | finalDropState(); 57 | return; 58 | } 59 | frames += 1; 60 | return setTimeout(openNextDrop, frameLengthMS * frames); 61 | }; 62 | finalDropState = function() { 63 | $(drops['top left'].dropContent).html('Marrying DOM elements for life.'); 64 | $(drops['bottom right'].dropContent).html('★ On Github'); 65 | drops['top left'].open(); 66 | return drops['bottom right'].open(); 67 | }; 68 | if (true || isMobile) { 69 | drops['top left'].open(); 70 | drops['top left'].tether.position(); 71 | drops['bottom right'].open(); 72 | drops['bottom right'].tether.position(); 73 | return finalDropState(); 74 | } else { 75 | return openNextDrop(); 76 | } 77 | }; 78 | 79 | setupBrowserDemo = function() { 80 | var $browserContents, $browserDemo, $iframe, $sections, $startPoint, $stopPoint, scrollInterval, scrollTop, scrollTopDirection, setSection; 81 | $browserDemo = $('.browser-demo.showcase'); 82 | $startPoint = $('.browser-demo-start-point'); 83 | $stopPoint = $('.browser-demo-stop-point'); 84 | $iframe = $('.browser-window iframe'); 85 | $browserContents = $('.browser-content .browser-demo-inner'); 86 | $sections = $('.browser-demo-section'); 87 | $('body').append(""); 88 | $(window).scroll(function() { 89 | var scrollTop; 90 | scrollTop = $(window).scrollTop(); 91 | if ($startPoint.position().top < scrollTop && scrollTop + window.innerHeight < $stopPoint.position().top) { 92 | $browserDemo.removeClass('fixed-bottom'); 93 | $browserDemo.addClass('fixed'); 94 | return $sections.each(function() { 95 | var $section; 96 | $section = $(this); 97 | if (($section.position().top < scrollTop && scrollTop < $section.position().top + $section.outerHeight())) { 98 | setSection($section.data('section')); 99 | } 100 | return true; 101 | }); 102 | } else { 103 | $browserDemo.removeAttr('data-section'); 104 | $browserDemo.removeClass('fixed'); 105 | if (scrollTop + window.innerHeight > $stopPoint.position().top) { 106 | return $browserDemo.addClass('fixed-bottom'); 107 | } else { 108 | return $browserDemo.removeClass('fixed-bottom'); 109 | } 110 | } 111 | }); 112 | $iframe.load(function() { 113 | var $items, iframeWindow; 114 | iframeWindow = $iframe[0].contentWindow; 115 | $items = $iframe.contents().find('.item'); 116 | return $items.each(function(i) { 117 | var $item, drop, _iframeWindowDrop; 118 | $item = $(this); 119 | _iframeWindowDrop = iframeWindow.Drop.createContext({ 120 | classPrefix: 'tether' 121 | }); 122 | drop = new _iframeWindowDrop({ 123 | target: $item[0], 124 | classes: 'tether-theme-arrows-dark', 125 | position: 'right top', 126 | constrainToWindow: true, 127 | openOn: 'click', 128 | content: '' 129 | }); 130 | return $item.data('drop', drop); 131 | }); 132 | }); 133 | scrollInterval = void 0; 134 | scrollTop = 0; 135 | scrollTopDirection = 1; 136 | return setSection = function(section) { 137 | var closeAllItems, openExampleItem, scrollLeftSection, stopScrollingLeftSection; 138 | $browserDemo.attr('data-section', section); 139 | $('.section-copy').removeClass('active'); 140 | $(".section-copy[data-section=\"" + section + "\"]").addClass('active'); 141 | openExampleItem = function() { 142 | if (isMobile) { 143 | return $iframe.contents().find('.item:first').data().drop.open(); 144 | } else { 145 | return $iframe.contents().find('.item:eq(2)').data().drop.open(); 146 | } 147 | }; 148 | closeAllItems = function() { 149 | return $iframe.contents().find('.item').each(function() { 150 | return $(this).data().drop.close() || true; 151 | }); 152 | }; 153 | scrollLeftSection = function() { 154 | return scrollInterval = setInterval(function() { 155 | $iframe.contents().find('.left').scrollTop(scrollTop); 156 | scrollTop += scrollTopDirection; 157 | if (scrollTop > 50) { 158 | scrollTopDirection = -1; 159 | } 160 | if (scrollTop < 0) { 161 | return scrollTopDirection = 1; 162 | } 163 | }, 30); 164 | }; 165 | stopScrollingLeftSection = function() { 166 | return clearInterval(scrollInterval); 167 | }; 168 | switch (section) { 169 | case 'what': 170 | closeAllItems(); 171 | openExampleItem(); 172 | return stopScrollingLeftSection(); 173 | case 'how': 174 | closeAllItems(); 175 | openExampleItem(); 176 | stopScrollingLeftSection(); 177 | return scrollLeftSection(); 178 | case 'why': 179 | closeAllItems(); 180 | openExampleItem(); 181 | stopScrollingLeftSection(); 182 | return scrollLeftSection(); 183 | case 'outro': 184 | closeAllItems(); 185 | openExampleItem(); 186 | return stopScrollingLeftSection(); 187 | } 188 | }; 189 | }; 190 | 191 | init(); 192 | 193 | }).call(this); 194 | -------------------------------------------------------------------------------- /docs/welcome/sass/_inline-block.sass: -------------------------------------------------------------------------------- 1 | @mixin inline-block 2 | display: inline-block 3 | vertical-align: middle 4 | *vertical-align: auto 5 | *zoom: 1 6 | *display: inline 7 | -------------------------------------------------------------------------------- /docs/welcome/sass/browser-demo.sass: -------------------------------------------------------------------------------- 1 | @import inline-block 2 | 3 | html, body 4 | height: 100% 5 | overflow: hidden 6 | font-family: "proxima-nova", sans-serif 7 | 8 | .tether.tether-theme-arrows-dark .tether-content 9 | filter: none 10 | background: #000 11 | 12 | ul 13 | color: #fff 14 | list-style: none 15 | padding: 0 16 | margin: 0 17 | 18 | .tether.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content:before 19 | border-right-color: #000 20 | 21 | .browser-demo 22 | position: absolute 23 | top: 0 24 | left: 0 25 | bottom: 0 26 | right: 0 27 | 28 | *, *:after, *:before 29 | box-sizing: border-box 30 | 31 | .top 32 | position: absolute 33 | height: 60px 34 | padding: 20px 35 | line-height: 40px 36 | width: 100% 37 | border-bottom: 1px solid rgba(0, 0, 0, .1) 38 | 39 | .bottom 40 | position: absolute 41 | top: 60px 42 | bottom: 0 43 | width: 100% 44 | 45 | .left 46 | border-right: 1px solid rgba(0, 0, 0, .1) 47 | position: absolute 48 | width: 30% 49 | height: 100% 50 | overflow: auto 51 | 52 | .item 53 | height: 64px 54 | border-bottom: 1px solid rgba(0, 0, 0, .1) 55 | cursor: pointer 56 | 57 | &:hover, &.tether-open 58 | background: rgba(0, 0, 0, .1) 59 | border-bottom: 1px solid rgba(0, 0, 0, 0) 60 | 61 | &:last-child 62 | border-bottom: 0 63 | 64 | .right 65 | position: absolute 66 | width: 70% 67 | right: 0 68 | height: 100% 69 | padding: 20px 70 | 71 | .title 72 | +inline-block 73 | background: rgba(0, 0, 0, .1) 74 | width: 150px 75 | height: 15px 76 | margin-bottom: 20px 77 | 78 | .word 79 | +inline-block 80 | background: rgba(0, 0, 0, .1) 81 | width: 50px 82 | height: 8px 83 | margin-right: 5px 84 | margin-bottom: 5px 85 | 86 | &:nth-last-child(4n+1) 87 | width: 73px 88 | 89 | &:nth-last-child(10n+1) 90 | width: 14px 91 | 92 | &:nth-last-child(9n+1) 93 | width: 80px 94 | -------------------------------------------------------------------------------- /docs/welcome/sass/welcome.sass: -------------------------------------------------------------------------------- 1 | @import inline-block 2 | 3 | html, body 4 | height: 100% 5 | 6 | body 7 | margin: 0 8 | font-family: "proxima-nova", "Helvetica Neue", sans-serif 9 | 10 | .button 11 | display: inline-block 12 | border: 2px solid #333 13 | color: #333 14 | padding: 1em 1.25em 15 | font-weight: 500 16 | text-transform: uppercase 17 | letter-spacing: 3px 18 | text-decoration: none 19 | cursor: pointer 20 | width: 140px 21 | font-size: .8em 22 | line-height: 1.3em 23 | text-align: center 24 | 25 | .tether-element.tether-theme-arrows-dark .tether-content 26 | padding: 1em 27 | font-size: 1.1em 28 | 29 | .button 30 | border-color: #fff 31 | color: #fff 32 | width: 170px 33 | pointer-events: all 34 | 35 | .mobile-copy 36 | display: none 37 | 38 | @media (max-width: 568px) 39 | display: block 40 | 41 | .button.dark 42 | background: #333 43 | color: #fff 44 | 45 | .hero-wrap 46 | height: 100% 47 | overflow: hidden 48 | 49 | table.showcase 50 | height: 100% 51 | width: 100% 52 | position: relative 53 | 54 | &:after 55 | content: "" 56 | display: block 57 | position: absolute 58 | left: 0 59 | right: 0 60 | bottom: 20px 61 | margin: auto 62 | height: 0 63 | width: 0 64 | border-width: 18px 65 | border-style: solid 66 | border-color: transparent 67 | border-top-color: rgba(0, 0, 0, 0.2) 68 | 69 | &.no-next-arrow:after 70 | display: none 71 | 72 | .showcase-inner 73 | margin: 40px auto 60px 74 | padding: 10px 75 | 76 | h1 77 | font-size: 50px 78 | text-align: center 79 | font-weight: 300 80 | 81 | @media (max-width: 567px) 82 | font-size: 40px 83 | 84 | h2 85 | font-size: 24px 86 | text-align: center 87 | font-weight: 300 88 | margin: 1em 0 1em 89 | 90 | @media (max-width: 567px) 91 | font-size: 14px 92 | 93 | p 94 | text-align: center 95 | 96 | &.hero 97 | text-align: center 98 | 99 | .tether-target-demo 100 | +inline-block 101 | border: 2px dotted #000 102 | margin: 5rem auto 103 | padding: 5rem 104 | 105 | @media (max-width: 567px) 106 | padding: 1rem 107 | 108 | &.share 109 | background: #f3f3f3 110 | 111 | &.projects-showcase .showcase-inner 112 | 113 | .projects-list 114 | width: 80% 115 | max-width: 1200px 116 | margin: 0 auto 117 | 118 | .project 119 | color: inherit 120 | text-decoration: none 121 | position: relative 122 | width: 50% 123 | float: left 124 | text-align: center 125 | margin-bottom: 2rem 126 | 127 | &:nth-child(odd) 128 | clear: left 129 | 130 | .os-icon 131 | width: 8rem 132 | height: 8rem 133 | margin-bottom: 1rem 134 | background-size: 100% 135 | 136 | h1 137 | font-size: 2.5rem 138 | 139 | p 140 | font-size: 1.3rem 141 | 142 | &.browser-demo 143 | background-image: linear-gradient(top left, #723362 0%, #9d223c 100%) 144 | background-color: #9d223c 145 | position: absolute 146 | top: 100% 147 | 148 | &.fixed 149 | position: fixed 150 | top: 0 151 | bottom: 0 152 | left: 0 153 | right: 0 154 | z-index: 1 155 | 156 | .browser-demo-inner 157 | transition: width 2s ease-in-out, height 2s ease-in-out 158 | 159 | // Sections 160 | 161 | &[data-section="what"] 162 | box-shadow: 0 0 0 0 163 | 164 | &[data-section="why"] 165 | 166 | .browser-demo-inner 167 | width: 70% 168 | 169 | &[data-section="outro"] 170 | 171 | .showcase-inner 172 | pointer-events: all 173 | 174 | .showcase-inner 175 | pointer-events: none 176 | position: absolute 177 | left: 10% 178 | right: 40% 179 | top: 220px 180 | bottom: 120px 181 | margin: 0 182 | padding: 0 183 | 184 | @media (max-width: 567px) 185 | bottom: 90px 186 | top: 180px 187 | 188 | .browser-demo-inner 189 | height: 100% 190 | width: 100% 191 | 192 | .section-copy 193 | transition: opacity .5s ease-in-out, top .5s ease-in-out 194 | opacity: 0 195 | position: absolute 196 | top: 0 197 | position: absolute 198 | height: 200px 199 | color: #fff 200 | text-align: center 201 | width: 100% 202 | 203 | &.active 204 | opacity: 1 205 | top: -150px 206 | 207 | @media (max-width: 567px) 208 | top: -130px 209 | 210 | h2 211 | font-size: 40px 212 | font-weight: bold 213 | line-height: 1 214 | margin: 25px 0 15px 215 | 216 | @media (max-width: 567px) 217 | font-size: 30px 218 | 219 | .browser-window 220 | border-radius: 4px 221 | background: #fff 222 | position: relative 223 | height: 100% 224 | width: 100% 225 | max-width: 1200px 226 | margin: 0 auto 227 | 228 | .browser-titlebar 229 | position: absolute 230 | top: 0 231 | left: 0 232 | right: 0 233 | border-bottom: 1px solid #eee 234 | height: 55px 235 | 236 | .browser-dots 237 | padding: 16px 238 | 239 | b 240 | +inline-block 241 | border-radius: 50% 242 | width: 10px 243 | height: 10px 244 | margin-right: 7px 245 | background: rgba(0, 0, 0, .1) 246 | 247 | .browser-frame 248 | position: absolute 249 | top: 55px 250 | left: 0 251 | right: 0 252 | bottom: 0 253 | 254 | iframe 255 | border-radius: 0 0 4px 4px 256 | border: 0 257 | width: 100% 258 | height: 100% 259 | 260 | &.browser-demo-section 261 | 262 | .section-scroll-copy 263 | position: relative 264 | z-index: 10 265 | color: #fff 266 | width: 100% 267 | font-size: 22px 268 | 269 | .section-scroll-copy-inner 270 | position: absolute 271 | z-index: 10 272 | color: #fff 273 | right: 10% 274 | width: 23% 275 | 276 | a 277 | color: inherit 278 | 279 | .example-paragraph 280 | border-radius: 4px 281 | background: #000 282 | padding: 1rem 283 | 284 | .browser-content 285 | display: none 286 | -------------------------------------------------------------------------------- /examples/common/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: 3000px; 3 | } 4 | .element { 5 | width: 200px; 6 | height: 200px; 7 | background-color: #fe8; 8 | position: absolute; 9 | z-index: 6; 10 | } 11 | 12 | .target { 13 | width: 300px; 14 | height: 50px; 15 | margin: 0 35%; 16 | background-color: #4e9; 17 | } 18 | 19 | .container { 20 | height: 600px; 21 | overflow: scroll; 22 | width: 600px; 23 | border: 20px solid #CCC; 24 | margin-top: 100px; 25 | } 26 | 27 | body { 28 | padding: 15px; 29 | } 30 | 31 | body > .container { 32 | margin: 0 auto; 33 | } 34 | 35 | .pad { 36 | height: 400px; 37 | width: 100px; 38 | } 39 | 40 | .instructions { 41 | width: 100%; 42 | text-align: center; 43 | font-size: 24px; 44 | padding: 15px; 45 | background-color: rgba(210, 180, 140, 0.4); 46 | margin: -15px -15px 0 -15px; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /examples/content-visible/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Scroll the page
9 | 10 | 44 | 45 |
46 |
47 |

This is some sort of crazy dialog.

48 | 49 |

It's setup to align with the center of the visible part of the blue area.

50 |
51 |
52 | 53 | 54 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/dolls/dolls.css: -------------------------------------------------------------------------------- 1 | .tether-element, .tether-target { 2 | width: 200px; 3 | height: 50px; 4 | background-color: #4cc; 5 | position: absolute; 6 | } 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | overflow: scroll; 11 | } 12 | .scroll { 13 | width: 400%; 14 | height: 400%; 15 | } 16 | .tether-target:not(.tether-element) { 17 | cursor: move; 18 | } 19 | -------------------------------------------------------------------------------- /examples/dolls/dolls.js: -------------------------------------------------------------------------------- 1 | var tethers = []; 2 | 3 | document.addEventListener('DOMContentLoaded', function(){ 4 | dragging = null; 5 | 6 | document.body.addEventListener('mouseup', function(){ 7 | dragging = null; 8 | }); 9 | 10 | document.body.addEventListener('mousemove', function(e){ 11 | if (dragging){ 12 | dragging.style.top = e.clientY + 'px'; 13 | dragging.style.left = e.clientX + 'px'; 14 | 15 | Tether.position() 16 | } 17 | }); 18 | 19 | document.body.addEventListener('mousedown', function(e){ 20 | if (e.target.getAttribute('data-index')) 21 | dragging = e.target; 22 | }) 23 | 24 | var count = 60; 25 | var parent = null; 26 | var dir = 'left'; 27 | var first = null; 28 | 29 | while (count--){ 30 | var el = document.createElement('div'); 31 | el.setAttribute('data-index', count); 32 | document.querySelector('.scroll').appendChild(el); 33 | 34 | if (!first) 35 | first = el; 36 | 37 | if (count % 10 === 0) 38 | dir = dir == 'right' ? 'left' : 'right'; 39 | 40 | if (parent){ 41 | tethers.push(new Tether({ 42 | element: el, 43 | target: parent, 44 | attachment: 'middle ' + dir, 45 | targetOffset: (dir == 'left' ? '10px 10px' : '10px -10px') 46 | })); 47 | 48 | } 49 | 50 | parent = el; 51 | } 52 | 53 | initAnim(first); 54 | }); 55 | 56 | function initAnim(el){ 57 | var start = performance.now() 58 | var last = 0; 59 | var lastTop = 0; 60 | var tick = function(){ 61 | var diff = performance.now() - last; 62 | 63 | if (!last || diff > 50){ 64 | last = performance.now(); 65 | 66 | var nextTop = 50 * Math.sin((last - start) / 1000); 67 | 68 | var curTop = parseFloat(el.style.top || 0); 69 | var topChange = nextTop - lastTop; 70 | lastTop = nextTop; 71 | 72 | var top = curTop + topChange; 73 | 74 | el.style.top = top + 'px'; 75 | 76 | Tether.position(); 77 | } 78 | 79 | requestAnimationFrame(tick); 80 | }; 81 | 82 | tick(); 83 | } 84 | -------------------------------------------------------------------------------- /examples/dolls/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /examples/enable-disable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Click the green target to enable/disable the tethering.
12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Examples 8 | 9 | 10 |

Examples

11 |

It's our goal to create a wide variety of example of how Tether 12 | can be used. Here's what we have so far, please send a PR with 13 | any examples you might create.

14 |

Beginner

15 | 23 |

Advanced

24 | 37 | 38 | -------------------------------------------------------------------------------- /examples/out-of-bounds/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
Resize the screen to see the tethered element disappear when it can't fit.
17 | 18 |
19 |
20 | 21 | 22 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/pin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Resize the screen to see the tethered element stick to the edges of the screen when it's resized.
12 | 13 |
14 |
15 | 16 | 17 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/resources/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", sans-serif; 3 | color: #444; 4 | margin: 0px; 5 | } 6 | 7 | a { 8 | cursor: pointer; 9 | color: blue; 10 | } -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Resize the page to see the Tether flip.
12 | 13 |
14 |
15 | 16 | 17 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/testbed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/viewport/colors.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /**** 3 | 4 | colors.css v1.0 For a friendlier looking web 5 | MIT License • http://clrs.cc • http://github.com/mrmrs/colors 6 | 7 | Author: mrmrs 8 | http://mrmrs.cc 9 | @mrmrs_ 10 | 11 | ****/ 12 | /* 13 | 14 | SKINS 15 | • Backgrounds 16 | • Colors 17 | 18 | */ 19 | /* Backgrounds */ 20 | .bg-navy { 21 | background-color: #001f3f; } 22 | 23 | .bg-blue { 24 | background-color: #0074d9; } 25 | 26 | .bg-aqua { 27 | background-color: #7fdbff; } 28 | 29 | .bg-teal { 30 | background-color: #39cccc; } 31 | 32 | .bg-olive { 33 | background-color: #3d9970; } 34 | 35 | .bg-green { 36 | background-color: #2ecc40; } 37 | 38 | .bg-lime { 39 | background-color: #01ff70; } 40 | 41 | .bg-yellow { 42 | background-color: #ffdc00; } 43 | 44 | .bg-orange { 45 | background-color: #ff851b; } 46 | 47 | .bg-red { 48 | background-color: #ff4136; } 49 | 50 | .bg-fuchsia { 51 | background-color: #f012be; } 52 | 53 | .bg-purple { 54 | background-color: #b10dc9; } 55 | 56 | .bg-maroon { 57 | background-color: #85144b; } 58 | 59 | .bg-white { 60 | background-color: white; } 61 | 62 | .bg-gray { 63 | background-color: #aaaaaa; } 64 | 65 | .bg-silver { 66 | background-color: #dddddd; } 67 | 68 | .bg-black { 69 | background-color: #111111; } 70 | 71 | /* Colors */ 72 | .navy { 73 | color: #001f3f; } 74 | 75 | .blue { 76 | color: #0074d9; } 77 | 78 | .aqua { 79 | color: #7fdbff; } 80 | 81 | .teal { 82 | color: #39cccc; } 83 | 84 | .olive { 85 | color: #3d9970; } 86 | 87 | .green { 88 | color: #2ecc40; } 89 | 90 | .lime { 91 | color: #01ff70; } 92 | 93 | .yellow { 94 | color: #ffdc00; } 95 | 96 | .orange { 97 | color: #ff851b; } 98 | 99 | .red { 100 | color: #ff4136; } 101 | 102 | .fuchsia { 103 | color: #f012be; } 104 | 105 | .purple { 106 | color: #b10dc9; } 107 | 108 | .maroon { 109 | color: #85144b; } 110 | 111 | .white { 112 | color: white; } 113 | 114 | .silver { 115 | color: #dddddd; } 116 | 117 | .gray { 118 | color: #aaaaaa; } 119 | 120 | .black { 121 | color: #111111; } 122 | 123 | /* PRETTIER LINKS */ 124 | a { 125 | text-decoration: none; 126 | -webkit-transition: color .3s ease-in-out; 127 | transition: color .3s ease-in-out; } 128 | 129 | a:link { 130 | color: #0074d9; 131 | -webkit-transition: color .3s ease-in-out; 132 | transition: color .3s ease-in-out; } 133 | 134 | a:visited { 135 | color: #b10dc9; } 136 | 137 | a:hover { 138 | color: #7fdbff; 139 | -webkit-transition: color .3s ease-in-out; 140 | transition: color .3s ease-in-out; } 141 | 142 | a:active { 143 | color: #ff851b; 144 | -webkit-transition: color .3s ease-in-out; 145 | transition: color .3s ease-in-out; } 146 | -------------------------------------------------------------------------------- /examples/viewport/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 32 | 33 | 34 |
35 |

This element is tethered to the middle of the visible part of the body.

36 | 37 |

Inspect the element to see how Tether decided 38 | to use position: fixed.

39 |
40 | 41 | 42 | 51 | 52 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // Automatically clear mock calls and instances between every test 6 | clearMocks: true, 7 | 8 | // An array of glob patterns indicating a set of files for which coverage information should be collected 9 | collectCoverageFrom: ['src/js/**/*.js'], 10 | 11 | // The directory where Jest should output its coverage files 12 | coverageDirectory: 'coverage', 13 | 14 | // An array of file extensions your modules use 15 | moduleFileExtensions: ['js'], 16 | 17 | moduleNameMapper: { 18 | '.+\\.(css|styl|less|sass|scss)$': '/__mocks__/styleMock.js' 19 | }, 20 | 21 | // The path to a module that runs some code to configure or set up the testing framework before each test 22 | setupFilesAfterEnv: ['/test/unit/setupTests.js'], 23 | 24 | testEnvironment: 'jsdom', 25 | 26 | testPathIgnorePatterns: ['/node_modules/', '/test/cypress/'], 27 | 28 | // A map from regular expressions to paths to transformers 29 | transform: { 30 | '^.+\\.js$': 'babel-jest' 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tether", 3 | "version": "2.0.0", 4 | "description": "A client-side library to make absolutely positioned elements attach to elements in the page efficiently.", 5 | "authors": [ 6 | "Zack Bloom ", 7 | "Adam Schwartz " 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/shipshapecode/tether.git" 12 | }, 13 | "license": "MIT", 14 | "maintainers": [ 15 | "Nicholas Hwang ", 16 | "Trevor Burnham " 17 | ], 18 | "main": "dist/js/tether.js", 19 | "module": "dist/js/tether.esm.js", 20 | "scripts": { 21 | "build": "yarn clean && rollup -c", 22 | "changelog": "github_changelog_generator -u shipshapecode -p tether --since-tag v1.4.7", 23 | "clean": "rimraf dist", 24 | "cy:open": "./node_modules/.bin/cypress open", 25 | "cy:run": "./node_modules/.bin/cypress run", 26 | "lint:js": "eslint . --ext js", 27 | "start": "yarn watch", 28 | "start-test-server": "http-server -p 9002", 29 | "test": "yarn lint:js && yarn test:ci", 30 | "test:ci": "yarn test:unit:ci && yarn test:cy:ci", 31 | "test:cy:ci": "yarn build && start-server-and-test start-test-server http://localhost:9002 cy:run", 32 | "test:cy:watch": "yarn build && start-server-and-test start-test-server http://localhost:9002 cy:open", 33 | "test:unit:ci": "jest --coverage", 34 | "test:unit:watch": "jest --watch", 35 | "watch": "yarn clean && rollup -c --environment DEVELOPMENT --watch" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.27.4", 39 | "@babel/preset-env": "^7.27.2", 40 | "@testing-library/jest-dom": "^5.17.0", 41 | "autoprefixer": "^10.4.21", 42 | "babel-jest": "^29.7.0", 43 | "babel-plugin-rewire": "^1.2.0", 44 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 45 | "chai": "^5.2.0", 46 | "cssnano": "^6.1.2", 47 | "cypress": "13.17.0", 48 | "eslint": "^8.57.1", 49 | "eslint-plugin-jest": "^28.3.0", 50 | "http-server": "^14.1.1", 51 | "jest": "^29.7.0", 52 | "jest-environment-jsdom": "^29.7.0", 53 | "jest-expect-message": "^1.1.3", 54 | "jsdom": "^22.1.0", 55 | "mutationobserver-shim": "^0.3.7", 56 | "postcss": "^8.5.4", 57 | "release-it": "^15.11.0", 58 | "release-it-lerna-changelog": "^5.0.0", 59 | "rimraf": "^5.0.7", 60 | "rollup": "^2.79.2", 61 | "rollup-plugin-babel": "^4.4.0", 62 | "rollup-plugin-browsersync": "^1.3.3", 63 | "rollup-plugin-eslint": "^7.0.0", 64 | "rollup-plugin-filesize": "^10.0.0", 65 | "rollup-plugin-license": "^3.6.0", 66 | "rollup-plugin-sass": "^1.15.2", 67 | "rollup-plugin-terser": "^7.0.2", 68 | "rollup-plugin-visualizer": "^5.12.0", 69 | "sinon": "^15.1.2", 70 | "start-server-and-test": "^2.0.12" 71 | }, 72 | "publishConfig": { 73 | "registry": "https://registry.npmjs.org" 74 | }, 75 | "engines": { 76 | "node": ">= 16" 77 | }, 78 | "volta": { 79 | "node": "16.0.0", 80 | "yarn": "1.22.10" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer'; 2 | import babel from 'rollup-plugin-babel'; 3 | import browsersync from 'rollup-plugin-browsersync'; 4 | import cssnano from 'cssnano'; 5 | import { eslint } from 'rollup-plugin-eslint'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | import license from 'rollup-plugin-license'; 8 | import postcss from 'postcss'; 9 | import sass from 'rollup-plugin-sass'; 10 | import { terser } from 'rollup-plugin-terser'; 11 | import visualizer from 'rollup-plugin-visualizer'; 12 | import fs from 'fs'; 13 | 14 | const pkg = require('./package.json'); 15 | const banner = ['/*!', pkg.name, pkg.version, '*/\n'].join(' '); 16 | 17 | const env = process.env.DEVELOPMENT ? 'development' : 'production'; 18 | 19 | function getSassOptions(minify = false) { 20 | const postcssPlugins = [ 21 | autoprefixer({ 22 | grid: false 23 | }) 24 | ]; 25 | 26 | if (minify) { 27 | postcssPlugins.push(cssnano()); 28 | } 29 | return { 30 | output(styles, styleNodes) { 31 | fs.mkdirSync('dist/css', { recursive: true }, (err) => { 32 | if (err) { 33 | throw err; 34 | } 35 | }); 36 | 37 | styleNodes.forEach(({ id, content }) => { 38 | const scssName = id.substring(id.lastIndexOf('/') + 1, id.length); 39 | const [name] = scssName.split('.'); 40 | fs.writeFileSync(`dist/css/${name}.${minify ? 'min.css' : 'css'}`, content); 41 | }); 42 | }, 43 | processor: (css) => postcss(postcssPlugins) 44 | .process(css, { 45 | from: undefined 46 | }) 47 | .then((result) => result.css) 48 | }; 49 | } 50 | 51 | const plugins = [ 52 | eslint({ 53 | include: '**/*.js' 54 | }), 55 | babel(), 56 | sass(getSassOptions(false)), 57 | license({ 58 | banner 59 | }), 60 | filesize(), 61 | visualizer() 62 | ]; 63 | 64 | // If we are running with --environment DEVELOPMENT, serve via browsersync for local development 65 | if (process.env.DEVELOPMENT) { 66 | plugins.push( 67 | browsersync({ 68 | host: 'localhost', 69 | watch: true, 70 | port: 3000, 71 | notify: false, 72 | open: true, 73 | server: { 74 | baseDir: 'examples', 75 | routes: { 76 | '/dist/js/tether.js': 'dist/js/tether.js', 77 | '/dist/css/tether-theme-arrows-dark.css': 78 | 'dist/css/tether-theme-arrows-dark.css' 79 | } 80 | } 81 | }) 82 | ); 83 | } 84 | 85 | const rollupBuilds = [ 86 | { 87 | input: './src/js/tether.js', 88 | output: [ 89 | { 90 | file: pkg.main, 91 | format: 'umd', 92 | name: 'Tether', 93 | sourcemap: true 94 | }, 95 | { 96 | file: pkg.module, 97 | format: 'esm', 98 | sourcemap: true 99 | } 100 | ], 101 | plugins 102 | } 103 | ]; 104 | 105 | rollupBuilds.push({ 106 | input: './src/js/tether.js', 107 | output: [ 108 | { 109 | file: 'dist/js/tether.min.js', 110 | format: 'umd', 111 | name: 'Tether', 112 | sourcemap: true 113 | }, 114 | { 115 | file: 'dist/js/tether.esm.min.js', 116 | format: 'esm', 117 | sourcemap: true 118 | } 119 | ], 120 | plugins: [ 121 | babel(), 122 | sass(getSassOptions(true)), 123 | terser(), 124 | license({ 125 | banner 126 | }), 127 | filesize(), 128 | visualizer() 129 | ] 130 | }); 131 | 132 | export default rollupBuilds; 133 | -------------------------------------------------------------------------------- /src/css/helpers/_tether-theme-arrows.scss: -------------------------------------------------------------------------------- 1 | @mixin tether-theme-arrows($themePrefix: "tether", $themeName: "arrows", $arrowSize: 16px, $arrowPointerEvents: null, $backgroundColor: #fff, $color: inherit, $useDropShadow: false) { 2 | .#{$themePrefix}-element.#{$themePrefix}-theme-#{$themeName} { 3 | max-width: 100%; 4 | max-height: 100%; 5 | 6 | .#{$themePrefix}-content { 7 | border-radius: 5px; 8 | position: relative; 9 | font-family: inherit; 10 | background: $backgroundColor; 11 | color: $color; 12 | padding: 1em; 13 | font-size: 1.1em; 14 | line-height: 1.5em; 15 | 16 | @if $useDropShadow { 17 | transform: translateZ(0); 18 | filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); 19 | } 20 | 21 | &:before { 22 | content: ""; 23 | display: block; 24 | position: absolute; 25 | width: 0; 26 | height: 0; 27 | border-color: transparent; 28 | border-width: $arrowSize; 29 | border-style: solid; 30 | pointer-events: $arrowPointerEvents; 31 | } 32 | } 33 | 34 | // Centers and middles 35 | 36 | &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { 37 | margin-bottom: $arrowSize; 38 | 39 | &:before { 40 | top: 100%; 41 | left: 50%; 42 | margin-left: -$arrowSize; 43 | border-top-color: $backgroundColor; 44 | border-bottom: 0; 45 | } 46 | } 47 | 48 | &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { 49 | margin-top: $arrowSize; 50 | 51 | &:before { 52 | bottom: 100%; 53 | left: 50%; 54 | margin-left: -$arrowSize; 55 | border-bottom-color: $backgroundColor; 56 | border-top: 0; 57 | } 58 | } 59 | 60 | &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { 61 | margin-right: $arrowSize; 62 | 63 | &:before { 64 | left: 100%; 65 | top: 50%; 66 | margin-top: -$arrowSize; 67 | border-left-color: $backgroundColor; 68 | border-right: 0; 69 | } 70 | } 71 | 72 | &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { 73 | margin-left: $arrowSize; 74 | 75 | &:before { 76 | right: 100%; 77 | top: 50%; 78 | margin-top: -$arrowSize; 79 | border-right-color: $backgroundColor; 80 | border-left: 0; 81 | } 82 | } 83 | 84 | // Target middle/center, element corner 85 | 86 | &.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-center .#{$themePrefix}-content { 87 | left: -$arrowSize * 2; 88 | } 89 | 90 | &.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-center .#{$themePrefix}-content { 91 | left: $arrowSize * 2; 92 | } 93 | 94 | &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content { 95 | margin-top: $arrowSize; 96 | 97 | &:before { 98 | bottom: 100%; 99 | left: $arrowSize; 100 | border-bottom-color: $backgroundColor; 101 | border-top: 0; 102 | } 103 | } 104 | 105 | &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content { 106 | margin-top: $arrowSize; 107 | 108 | &:before { 109 | bottom: 100%; 110 | right: $arrowSize; 111 | border-bottom-color: $backgroundColor; 112 | border-top: 0; 113 | } 114 | } 115 | 116 | &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content { 117 | margin-bottom: $arrowSize; 118 | 119 | &:before { 120 | top: 100%; 121 | left: $arrowSize; 122 | border-top-color: $backgroundColor; 123 | border-bottom: 0; 124 | } 125 | } 126 | 127 | &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-middle .#{$themePrefix}-content { 128 | margin-bottom: $arrowSize; 129 | 130 | &:before { 131 | top: 100%; 132 | right: $arrowSize; 133 | border-top-color: $backgroundColor; 134 | border-bottom: 0; 135 | } 136 | } 137 | 138 | // Top and bottom corners 139 | 140 | &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content { 141 | margin-top: $arrowSize; 142 | 143 | &:before { 144 | bottom: 100%; 145 | left: $arrowSize; 146 | border-bottom-color: $backgroundColor; 147 | border-top: 0; 148 | } 149 | } 150 | 151 | &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content { 152 | margin-top: $arrowSize; 153 | 154 | &:before { 155 | bottom: 100%; 156 | right: $arrowSize; 157 | border-bottom-color: $backgroundColor; 158 | border-top: 0; 159 | } 160 | } 161 | 162 | &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-top .#{$themePrefix}-content { 163 | margin-bottom: $arrowSize; 164 | 165 | &:before { 166 | top: 100%; 167 | left: $arrowSize; 168 | border-top-color: $backgroundColor; 169 | border-bottom: 0; 170 | } 171 | } 172 | 173 | &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-top .#{$themePrefix}-content { 174 | margin-bottom: $arrowSize; 175 | 176 | &:before { 177 | top: 100%; 178 | right: $arrowSize; 179 | border-top-color: $backgroundColor; 180 | border-bottom: 0; 181 | } 182 | } 183 | 184 | // Side corners 185 | 186 | &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content { 187 | margin-right: $arrowSize; 188 | 189 | &:before { 190 | top: $arrowSize; 191 | left: 100%; 192 | border-left-color: $backgroundColor; 193 | border-right: 0; 194 | } 195 | } 196 | 197 | &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content { 198 | margin-left: $arrowSize; 199 | 200 | &:before { 201 | top: $arrowSize; 202 | right: 100%; 203 | border-right-color: $backgroundColor; 204 | border-left: 0; 205 | } 206 | } 207 | 208 | &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content { 209 | margin-right: $arrowSize; 210 | 211 | &:before { 212 | bottom: $arrowSize; 213 | left: 100%; 214 | border-left-color: $backgroundColor; 215 | border-right: 0; 216 | } 217 | } 218 | 219 | &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content { 220 | margin-left: $arrowSize; 221 | 222 | &:before { 223 | bottom: $arrowSize; 224 | right: 100%; 225 | border-right-color: $backgroundColor; 226 | border-left: 0; 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/css/helpers/_tether-theme-basic.scss: -------------------------------------------------------------------------------- 1 | @mixin tether-theme-basic($themePrefix: "tether", $themeName: "basic", $backgroundColor: #fff, $color: inherit) { 2 | .#{$themePrefix}-element.#{$themePrefix}-theme-#{$themeName} { 3 | max-width: 100%; 4 | max-height: 100%; 5 | 6 | .#{$themePrefix}-content { 7 | border-radius: 5px; 8 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 9 | font-family: inherit; 10 | background: $backgroundColor; 11 | color: $color; 12 | padding: 1em; 13 | font-size: 1.1em; 14 | line-height: 1.5em; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/css/helpers/_tether.scss: -------------------------------------------------------------------------------- 1 | @mixin tether($themePrefix: "tether") { 2 | .#{$themePrefix}-element, .#{$themePrefix}-element * { 3 | &, &:after, &:before { 4 | box-sizing: border-box; 5 | } 6 | } 7 | 8 | .#{$themePrefix}-element { 9 | position: absolute; 10 | display: none; 11 | 12 | &.#{$themePrefix}-open { 13 | display: block; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/css/mixins/_inline-block.scss: -------------------------------------------------------------------------------- 1 | @mixin inline-block { 2 | display: inline-block; 3 | vertical-align: middle; 4 | *vertical-align: auto; 5 | *zoom: 1; 6 | *display: inline; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/mixins/_pie-clearfix.scss: -------------------------------------------------------------------------------- 1 | @mixin pie-clearfix { 2 | *zoom: 1; 3 | 4 | &:after { 5 | content: ""; 6 | display: table; 7 | clear: both; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/css/tether-theme-arrows-dark.scss: -------------------------------------------------------------------------------- 1 | @import "helpers/tether"; 2 | @import "helpers/tether-theme-arrows"; 3 | 4 | $themePrefix: "tether"; 5 | $themeName: "arrows-dark"; 6 | $arrowSize: 16px; 7 | $backgroundColor: #000; 8 | $color: #fff; 9 | $useDropShadow: false; 10 | 11 | @include tether($themePrefix: $themePrefix); 12 | @include tether-theme-arrows($themePrefix: $themePrefix, $themeName: $themeName, $arrowSize: $arrowSize, $backgroundColor: $backgroundColor, $color: $color, $useDropShadow: $useDropShadow); 13 | -------------------------------------------------------------------------------- /src/css/tether-theme-arrows.scss: -------------------------------------------------------------------------------- 1 | @import "helpers/tether"; 2 | @import "helpers/tether-theme-arrows"; 3 | 4 | $themePrefix: "tether"; 5 | $themeName: "arrows"; 6 | $arrowSize: 16px; 7 | $backgroundColor: #fff; 8 | $color: inherit; 9 | $useDropShadow: true; 10 | 11 | @include tether($themePrefix: $themePrefix); 12 | @include tether-theme-arrows($themePrefix: $themePrefix, $themeName: $themeName, $arrowSize: $arrowSize, $backgroundColor: $backgroundColor, $color: $color, $useDropShadow: $useDropShadow); 13 | -------------------------------------------------------------------------------- /src/css/tether-theme-basic.scss: -------------------------------------------------------------------------------- 1 | @import "helpers/tether"; 2 | @import "helpers/tether-theme-basic"; 3 | 4 | $themePrefix: "tether"; 5 | $themeName: "basic"; 6 | $backgroundColor: #fff; 7 | $color: inherit; 8 | 9 | @include tether($themePrefix: $themePrefix); 10 | @include tether-theme-basic($themePrefix: $themePrefix, $themeName: $themeName, $backgroundColor: $backgroundColor, $color: $color); 11 | -------------------------------------------------------------------------------- /src/css/tether.scss: -------------------------------------------------------------------------------- 1 | @import "helpers/tether"; 2 | 3 | $themePrefix: "tether"; 4 | 5 | @include tether($themePrefix: $themePrefix); 6 | -------------------------------------------------------------------------------- /src/js/abutment.js: -------------------------------------------------------------------------------- 1 | import { getClass, updateClasses } from './utils/classes'; 2 | import { defer } from './utils/deferred'; 3 | import { getBounds } from './utils/bounds'; 4 | 5 | export default { 6 | position({ top, left }) { 7 | const { height, width } = this.cache('element-bounds', () => { 8 | return getBounds(this.element); 9 | }); 10 | 11 | const targetPos = this.getTargetBounds(); 12 | 13 | const bottom = top + height; 14 | const right = left + width; 15 | 16 | const abutted = []; 17 | if (top <= targetPos.bottom && bottom >= targetPos.top) { 18 | ['left', 'right'].forEach((side) => { 19 | const targetPosSide = targetPos[side]; 20 | if (targetPosSide === left || targetPosSide === right) { 21 | abutted.push(side); 22 | } 23 | }); 24 | } 25 | 26 | if (left <= targetPos.right && right >= targetPos.left) { 27 | ['top', 'bottom'].forEach((side) => { 28 | const targetPosSide = targetPos[side]; 29 | if (targetPosSide === top || targetPosSide === bottom) { 30 | abutted.push(side); 31 | } 32 | }); 33 | } 34 | 35 | const sides = ['left', 'top', 'right', 'bottom']; 36 | const { classes, classPrefix } = this.options; 37 | this.all.push(getClass('abutted', classes, classPrefix)); 38 | sides.forEach((side) => { 39 | this.all.push(`${getClass('abutted', classes, classPrefix)}-${side}`); 40 | }); 41 | 42 | if (abutted.length) { 43 | this.add.push(getClass('abutted', classes, classPrefix)); 44 | } 45 | 46 | abutted.forEach((side) => { 47 | this.add.push(`${getClass('abutted', classes, classPrefix)}-${side}`); 48 | }); 49 | 50 | defer(() => { 51 | if (!(this.options.addTargetClasses === false)) { 52 | updateClasses(this.target, this.add, this.all); 53 | } 54 | updateClasses(this.element, this.add, this.all); 55 | }); 56 | 57 | return true; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/js/constraint.js: -------------------------------------------------------------------------------- 1 | import { getClass, updateClasses } from './utils/classes'; 2 | import { defer } from './utils/deferred'; 3 | import { extend } from './utils/general'; 4 | import { getBounds } from './utils/bounds'; 5 | import { isString, isUndefined } from './utils/type-check'; 6 | 7 | const BOUNDS_FORMAT = ['left', 'top', 'right', 'bottom']; 8 | 9 | /** 10 | * Returns an array of bounds of the format [left, top, right, bottom] 11 | * @param tether 12 | * @param to 13 | * @return {*[]|HTMLElement|ActiveX.IXMLDOMElement} 14 | */ 15 | function getBoundingRect(body, tether, to) { 16 | // arg to is required 17 | if (!to) { 18 | return null; 19 | } 20 | if (to === 'scrollParent') { 21 | to = tether.scrollParents[0]; 22 | } else if (to === 'window') { 23 | to = [pageXOffset, pageYOffset, innerWidth + pageXOffset, innerHeight + pageYOffset]; 24 | } 25 | 26 | if (to === document) { 27 | to = to.documentElement; 28 | } 29 | 30 | if (!isUndefined(to.nodeType)) { 31 | const node = to; 32 | const size = getBounds(body, to); 33 | const pos = size; 34 | const style = getComputedStyle(to); 35 | 36 | to = [pos.left, pos.top, size.width + pos.left, size.height + pos.top]; 37 | 38 | // Account any parent Frames scroll offset 39 | if (node.ownerDocument !== document) { 40 | let win = node.ownerDocument.defaultView; 41 | to[0] += win.pageXOffset; 42 | to[1] += win.pageYOffset; 43 | to[2] += win.pageXOffset; 44 | to[3] += win.pageYOffset; 45 | } 46 | 47 | BOUNDS_FORMAT.forEach((side, i) => { 48 | side = side[0].toUpperCase() + side.substr(1); 49 | if (side === 'Top' || side === 'Left') { 50 | to[i] += parseFloat(style[`border${side}Width`]); 51 | } else { 52 | to[i] -= parseFloat(style[`border${side}Width`]); 53 | } 54 | }); 55 | } 56 | 57 | return to; 58 | } 59 | 60 | /** 61 | * Add out of bounds classes to the list of classes we add to tether 62 | * @param {string[]} oob An array of directions that are out of bounds 63 | * @param {string[]} addClasses The array of classes to add to Tether 64 | * @param {string[]} classes The array of class types for Tether 65 | * @param {string} classPrefix The prefix to add to the front of the class 66 | * @param {string} outOfBoundsClass The class to apply when out of bounds 67 | * @private 68 | */ 69 | function _addOutOfBoundsClass(oob, addClasses, classes, classPrefix, outOfBoundsClass) { 70 | if (oob.length) { 71 | let oobClass; 72 | if (!isUndefined(outOfBoundsClass)) { 73 | oobClass = outOfBoundsClass; 74 | } else { 75 | oobClass = getClass('out-of-bounds', classes, classPrefix); 76 | } 77 | 78 | addClasses.push(oobClass); 79 | oob.forEach((side) => { 80 | addClasses.push(`${oobClass}-${side}`); 81 | }); 82 | } 83 | } 84 | 85 | /** 86 | * Calculates if out of bounds or pinned in the X direction. 87 | * 88 | * @param {number} left 89 | * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom] 90 | * @param {number} width 91 | * @param pin 92 | * @param pinned 93 | * @param {string[]} oob 94 | * @return {number} 95 | * @private 96 | */ 97 | function _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob) { 98 | if (left < bounds[0]) { 99 | if (pin.indexOf('left') >= 0) { 100 | left = bounds[0]; 101 | pinned.push('left'); 102 | } else { 103 | oob.push('left'); 104 | } 105 | } 106 | 107 | if (left + width > bounds[2]) { 108 | if (pin.indexOf('right') >= 0) { 109 | left = bounds[2] - width; 110 | pinned.push('right'); 111 | } else { 112 | oob.push('right'); 113 | } 114 | } 115 | 116 | return left; 117 | } 118 | 119 | /** 120 | * Calculates if out of bounds or pinned in the Y direction. 121 | * 122 | * @param {number} top 123 | * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom] 124 | * @param {number} height 125 | * @param pin 126 | * @param {string[]} pinned 127 | * @param {string[]} oob 128 | * @return {number} 129 | * @private 130 | */ 131 | function _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob) { 132 | if (top < bounds[1]) { 133 | if (pin.indexOf('top') >= 0) { 134 | top = bounds[1]; 135 | pinned.push('top'); 136 | } else { 137 | oob.push('top'); 138 | } 139 | } 140 | 141 | if (top + height > bounds[3]) { 142 | if (pin.indexOf('bottom') >= 0) { 143 | top = bounds[3] - height; 144 | pinned.push('bottom'); 145 | } else { 146 | oob.push('bottom'); 147 | } 148 | } 149 | 150 | return top; 151 | } 152 | 153 | /** 154 | * Flip X "together" 155 | * @param {object} tAttachment The target attachment 156 | * @param {object} eAttachment The element attachment 157 | * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom] 158 | * @param {number} width 159 | * @param targetWidth 160 | * @param {number} left 161 | * @private 162 | */ 163 | function _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left) { 164 | if (left < bounds[0] && tAttachment.left === 'left') { 165 | if (eAttachment.left === 'right') { 166 | left += targetWidth; 167 | tAttachment.left = 'right'; 168 | 169 | left += width; 170 | eAttachment.left = 'left'; 171 | 172 | } else if (eAttachment.left === 'left') { 173 | left += targetWidth; 174 | tAttachment.left = 'right'; 175 | 176 | left -= width; 177 | eAttachment.left = 'right'; 178 | } 179 | 180 | } else if (left + width > bounds[2] && tAttachment.left === 'right') { 181 | if (eAttachment.left === 'left') { 182 | left -= targetWidth; 183 | tAttachment.left = 'left'; 184 | 185 | left -= width; 186 | eAttachment.left = 'right'; 187 | 188 | } else if (eAttachment.left === 'right') { 189 | left -= targetWidth; 190 | tAttachment.left = 'left'; 191 | 192 | left += width; 193 | eAttachment.left = 'left'; 194 | } 195 | 196 | } else if (tAttachment.left === 'center') { 197 | if (left + width > bounds[2] && eAttachment.left === 'left') { 198 | left -= width; 199 | eAttachment.left = 'right'; 200 | 201 | } else if (left < bounds[0] && eAttachment.left === 'right') { 202 | left += width; 203 | eAttachment.left = 'left'; 204 | } 205 | } 206 | 207 | return left; 208 | } 209 | 210 | /** 211 | * Flip Y "together" 212 | * @param {object} tAttachment The target attachment 213 | * @param {object} eAttachment The element attachment 214 | * @param {number[]} bounds Array of bounds of the format [left, top, right, bottom] 215 | * @param {number} height 216 | * @param targetHeight 217 | * @param {number} top 218 | * @private 219 | */ 220 | function _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top) { 221 | if (tAttachment.top === 'top') { 222 | if (eAttachment.top === 'bottom' && top < bounds[1]) { 223 | top += targetHeight; 224 | tAttachment.top = 'bottom'; 225 | 226 | top += height; 227 | eAttachment.top = 'top'; 228 | 229 | } else if (eAttachment.top === 'top' && top + height > bounds[3] && top - (height - targetHeight) >= bounds[1]) { 230 | top -= height - targetHeight; 231 | tAttachment.top = 'bottom'; 232 | 233 | eAttachment.top = 'bottom'; 234 | } 235 | } 236 | 237 | if (tAttachment.top === 'bottom') { 238 | if (eAttachment.top === 'top' && top + height > bounds[3]) { 239 | top -= targetHeight; 240 | tAttachment.top = 'top'; 241 | 242 | top -= height; 243 | eAttachment.top = 'bottom'; 244 | 245 | } else if (eAttachment.top === 'bottom' && top < bounds[1] && top + (height * 2 - targetHeight) <= bounds[3]) { 246 | top += height - targetHeight; 247 | tAttachment.top = 'top'; 248 | 249 | eAttachment.top = 'top'; 250 | 251 | } 252 | } 253 | 254 | if (tAttachment.top === 'middle') { 255 | if (top + height > bounds[3] && eAttachment.top === 'top') { 256 | top -= height; 257 | eAttachment.top = 'bottom'; 258 | 259 | } else if (top < bounds[1] && eAttachment.top === 'bottom') { 260 | top += height; 261 | eAttachment.top = 'top'; 262 | } 263 | } 264 | 265 | return top; 266 | } 267 | 268 | /** 269 | * Get all the initial classes 270 | * @param classes 271 | * @param {string} classPrefix 272 | * @param constraints 273 | * @return {[*, *]} 274 | * @private 275 | */ 276 | function _getAllClasses(classes, classPrefix, constraints) { 277 | const allClasses = [getClass('pinned', classes, classPrefix), getClass('out-of-bounds', classes, classPrefix)]; 278 | 279 | constraints.forEach((constraint) => { 280 | const { outOfBoundsClass, pinnedClass } = constraint; 281 | if (outOfBoundsClass) { 282 | allClasses.push(outOfBoundsClass); 283 | } 284 | if (pinnedClass) { 285 | allClasses.push(pinnedClass); 286 | } 287 | }); 288 | 289 | allClasses.forEach((cls) => { 290 | ['left', 'top', 'right', 'bottom'].forEach((side) => { 291 | allClasses.push(`${cls}-${side}`); 292 | }); 293 | }); 294 | 295 | return allClasses; 296 | } 297 | 298 | export default { 299 | position({ top, left, targetAttachment }) { 300 | if (!this.options.constraints) { 301 | return true; 302 | } 303 | 304 | let { height, width } = this.cache('element-bounds', () => { 305 | return getBounds(this.bodyElement, this.element); 306 | }); 307 | 308 | if (width === 0 && height === 0 && !isUndefined(this.lastSize)) { 309 | // Handle the item getting hidden as a result of our positioning without glitching 310 | // the classes in and out 311 | ({ width, height } = this.lastSize); 312 | } 313 | 314 | const targetSize = this.cache('target-bounds', () => { 315 | return this.getTargetBounds(); 316 | }); 317 | 318 | const { height: targetHeight, width: targetWidth } = targetSize; 319 | const { classes, classPrefix } = this.options; 320 | 321 | const allClasses = _getAllClasses(classes, classPrefix, this.options.constraints); 322 | const addClasses = []; 323 | 324 | const tAttachment = extend({}, targetAttachment); 325 | const eAttachment = extend({}, this.attachment); 326 | 327 | this.options.constraints.forEach((constraint) => { 328 | let { to, attachment, pin } = constraint; 329 | 330 | if (isUndefined(attachment)) { 331 | attachment = ''; 332 | } 333 | 334 | let changeAttachX, changeAttachY; 335 | if (attachment.indexOf(' ') >= 0) { 336 | [changeAttachY, changeAttachX] = attachment.split(' '); 337 | } else { 338 | changeAttachX = changeAttachY = attachment; 339 | } 340 | 341 | const bounds = getBoundingRect(this.bodyElement, this, to); 342 | 343 | if (changeAttachY === 'target' || changeAttachY === 'both') { 344 | if (top < bounds[1] && tAttachment.top === 'top') { 345 | top += targetHeight; 346 | tAttachment.top = 'bottom'; 347 | } 348 | 349 | if (top + height > bounds[3] && tAttachment.top === 'bottom') { 350 | top -= targetHeight; 351 | tAttachment.top = 'top'; 352 | } 353 | } 354 | 355 | if (changeAttachY === 'together') { 356 | top = _flipYTogether(tAttachment, eAttachment, bounds, height, targetHeight, top); 357 | } 358 | 359 | if (changeAttachX === 'target' || changeAttachX === 'both') { 360 | if (left < bounds[0] && tAttachment.left === 'left') { 361 | left += targetWidth; 362 | tAttachment.left = 'right'; 363 | } 364 | 365 | if (left + width > bounds[2] && tAttachment.left === 'right') { 366 | left -= targetWidth; 367 | tAttachment.left = 'left'; 368 | } 369 | } 370 | 371 | if (changeAttachX === 'together') { 372 | left = _flipXTogether(tAttachment, eAttachment, bounds, width, targetWidth, left); 373 | } 374 | 375 | if (changeAttachY === 'element' || changeAttachY === 'both') { 376 | if (top < bounds[1] && eAttachment.top === 'bottom') { 377 | top += height; 378 | eAttachment.top = 'top'; 379 | } 380 | 381 | if (top + height > bounds[3] && eAttachment.top === 'top') { 382 | top -= height; 383 | eAttachment.top = 'bottom'; 384 | } 385 | } 386 | 387 | if (changeAttachX === 'element' || changeAttachX === 'both') { 388 | if (left < bounds[0]) { 389 | if (eAttachment.left === 'right') { 390 | left += width; 391 | eAttachment.left = 'left'; 392 | } else if (eAttachment.left === 'center') { 393 | left += (width / 2); 394 | eAttachment.left = 'left'; 395 | } 396 | } 397 | 398 | if (left + width > bounds[2]) { 399 | if (eAttachment.left === 'left') { 400 | left -= width; 401 | eAttachment.left = 'right'; 402 | } else if (eAttachment.left === 'center') { 403 | left -= (width / 2); 404 | eAttachment.left = 'right'; 405 | } 406 | } 407 | } 408 | 409 | if (isString(pin)) { 410 | pin = pin.split(',').map((p) => p.trim()); 411 | } else if (pin === true) { 412 | pin = ['top', 'left', 'right', 'bottom']; 413 | } 414 | 415 | pin = pin || []; 416 | 417 | const pinned = []; 418 | const oob = []; 419 | 420 | left = _calculateOOBAndPinnedLeft(left, bounds, width, pin, pinned, oob); 421 | top = _calculateOOBAndPinnedTop(top, bounds, height, pin, pinned, oob); 422 | 423 | if (pinned.length) { 424 | let pinnedClass; 425 | if (!isUndefined(this.options.pinnedClass)) { 426 | pinnedClass = this.options.pinnedClass; 427 | } else { 428 | pinnedClass = getClass('pinned', classes, classPrefix); 429 | } 430 | 431 | addClasses.push(pinnedClass); 432 | pinned.forEach((side) => { 433 | addClasses.push(`${pinnedClass}-${side}`); 434 | }); 435 | } 436 | 437 | _addOutOfBoundsClass(oob, addClasses, classes, classPrefix, this.options.outOfBoundsClass); 438 | 439 | if (pinned.indexOf('left') >= 0 || pinned.indexOf('right') >= 0) { 440 | eAttachment.left = tAttachment.left = false; 441 | } 442 | if (pinned.indexOf('top') >= 0 || pinned.indexOf('bottom') >= 0) { 443 | eAttachment.top = tAttachment.top = false; 444 | } 445 | 446 | if (tAttachment.top !== targetAttachment.top || 447 | tAttachment.left !== targetAttachment.left || 448 | eAttachment.top !== this.attachment.top || 449 | eAttachment.left !== this.attachment.left) { 450 | this.updateAttachClasses(eAttachment, tAttachment); 451 | this.trigger('update', { 452 | attachment: eAttachment, 453 | targetAttachment: tAttachment 454 | }); 455 | } 456 | }); 457 | 458 | defer(() => { 459 | if (!(this.options.addTargetClasses === false)) { 460 | updateClasses(this.target, addClasses, allClasses); 461 | } 462 | updateClasses(this.element, addClasses, allClasses); 463 | }); 464 | 465 | return { top, left }; 466 | } 467 | }; 468 | -------------------------------------------------------------------------------- /src/js/evented.js: -------------------------------------------------------------------------------- 1 | import { isUndefined } from './utils/type-check'; 2 | 3 | export class Evented { 4 | on(event, handler, ctx, once = false) { 5 | if (isUndefined(this.bindings)) { 6 | this.bindings = {}; 7 | } 8 | if (isUndefined(this.bindings[event])) { 9 | this.bindings[event] = []; 10 | } 11 | this.bindings[event].push({ handler, ctx, once }); 12 | 13 | return this; 14 | } 15 | 16 | once(event, handler, ctx) { 17 | return this.on(event, handler, ctx, true); 18 | } 19 | 20 | off(event, handler) { 21 | if (isUndefined(this.bindings) || 22 | isUndefined(this.bindings[event])) { 23 | return this; 24 | } 25 | 26 | if (isUndefined(handler)) { 27 | delete this.bindings[event]; 28 | } else { 29 | this.bindings[event].forEach((binding, index) => { 30 | if (binding.handler === handler) { 31 | this.bindings[event].splice(index, 1); 32 | } 33 | }); 34 | } 35 | 36 | return this; 37 | } 38 | 39 | trigger(event, ...args) { 40 | if (!isUndefined(this.bindings) && this.bindings[event]) { 41 | this.bindings[event].forEach((binding, index) => { 42 | const { ctx, handler, once } = binding; 43 | 44 | const context = ctx || this; 45 | 46 | handler.apply(context, args); 47 | 48 | if (once) { 49 | this.bindings[event].splice(index, 1); 50 | } 51 | }); 52 | } 53 | 54 | return this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/js/shift.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isString } from './utils/type-check'; 2 | 3 | export default { 4 | position({ top, left }) { 5 | if (!this.options.shift) { 6 | return; 7 | } 8 | 9 | let { shift } = this.options; 10 | if (isFunction(shift)) { 11 | shift = shift.call(this, { top, left }); 12 | } 13 | 14 | let shiftTop, shiftLeft; 15 | if (isString(shift)) { 16 | shift = shift.split(' '); 17 | shift[1] = shift[1] || shift[0]; 18 | 19 | ([shiftTop, shiftLeft] = shift); 20 | 21 | shiftTop = parseFloat(shiftTop, 10); 22 | shiftLeft = parseFloat(shiftLeft, 10); 23 | } else { 24 | ([shiftTop, shiftLeft] = [shift.top, shift.left]); 25 | } 26 | 27 | top += shiftTop; 28 | left += shiftLeft; 29 | 30 | return { top, left }; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/js/utils/bounds.js: -------------------------------------------------------------------------------- 1 | import { defer } from './deferred'; 2 | import { extend, uniqueId } from './general'; 3 | import { isUndefined } from './type-check'; 4 | 5 | const zeroPosCache = {}; 6 | let zeroElement = null; 7 | 8 | export function getBounds(body, el) { 9 | let doc; 10 | if (el === document) { 11 | doc = document; 12 | el = document.documentElement; 13 | } else { 14 | doc = el.ownerDocument; 15 | } 16 | 17 | const docEl = doc.documentElement; 18 | 19 | const box = _getActualBoundingClientRect(el); 20 | 21 | const origin = _getOrigin(body); 22 | 23 | box.top -= origin.top; 24 | box.left -= origin.left; 25 | 26 | if (isUndefined(box.width)) { 27 | box.width = document.body.scrollWidth - box.left - box.right; 28 | } 29 | if (isUndefined(box.height)) { 30 | box.height = document.body.scrollHeight - box.top - box.bottom; 31 | } 32 | 33 | box.top = box.top - docEl.clientTop; 34 | box.left = box.left - docEl.clientLeft; 35 | box.right = doc.body.clientWidth - box.width - box.left; 36 | box.bottom = doc.body.clientHeight - box.height - box.top; 37 | 38 | return box; 39 | } 40 | 41 | /** 42 | * Gets bounds for when target modifiier is 'scroll-handle' 43 | * @param target 44 | * @return {{left: number, width: number, height: number}} 45 | */ 46 | export function getScrollHandleBounds(body, target) { 47 | let bounds; 48 | // We have to do the check for the scrollTop and if target === document.body here and set to variables 49 | // because we may reset target below. 50 | const targetScrollTop = target.scrollTop; 51 | const targetIsBody = target === document.body; 52 | 53 | if (targetIsBody) { 54 | target = document.documentElement; 55 | 56 | bounds = { 57 | left: pageXOffset, 58 | top: pageYOffset, 59 | height: innerHeight, 60 | width: innerWidth 61 | }; 62 | } else { 63 | bounds = getBounds(body, target); 64 | } 65 | 66 | const style = getComputedStyle(target); 67 | 68 | const hasBottomScroll = ( 69 | target.scrollWidth > target.clientWidth || 70 | [style.overflow, style.overflowX].indexOf('scroll') >= 0 || 71 | !targetIsBody 72 | ); 73 | 74 | let scrollBottom = 0; 75 | if (hasBottomScroll) { 76 | scrollBottom = 15; 77 | } 78 | 79 | const height = bounds.height - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth) - scrollBottom; 80 | 81 | const out = { 82 | width: 15, 83 | height: height * 0.975 * (height / target.scrollHeight), 84 | left: bounds.left + bounds.width - parseFloat(style.borderLeftWidth) - 15 85 | }; 86 | 87 | let fitAdj = 0; 88 | if (height < 408 && targetIsBody) { 89 | fitAdj = -0.00011 * Math.pow(height, 2) - 0.00727 * height + 22.58; 90 | } 91 | 92 | if (!targetIsBody) { 93 | out.height = Math.max(out.height, 24); 94 | } 95 | 96 | const scrollPercentage = targetScrollTop / (target.scrollHeight - height); 97 | out.top = scrollPercentage * (height - out.height - fitAdj) + bounds.top + parseFloat(style.borderTopWidth); 98 | 99 | if (targetIsBody) { 100 | out.height = Math.max(out.height, 24); 101 | } 102 | 103 | return out; 104 | } 105 | 106 | /** 107 | * Gets bounds for when target modifiier is 'visible 108 | * @param target 109 | * @return {{top: *, left: *, width: *, height: *}} 110 | */ 111 | export function getVisibleBounds(body, target) { 112 | if (target === document.body) { 113 | return { top: pageYOffset, left: pageXOffset, height: innerHeight, width: innerWidth }; 114 | } else { 115 | const bounds = getBounds(body, target); 116 | 117 | const out = { 118 | height: bounds.height, 119 | width: bounds.width, 120 | top: bounds.top, 121 | left: bounds.left 122 | }; 123 | 124 | out.height = Math.min(out.height, bounds.height - (pageYOffset - bounds.top)); 125 | out.height = Math.min(out.height, bounds.height - ((bounds.top + bounds.height) - (pageYOffset + innerHeight))); 126 | out.height = Math.min(innerHeight, out.height); 127 | out.height -= 2; 128 | 129 | out.width = Math.min(out.width, bounds.width - (pageXOffset - bounds.left)); 130 | out.width = Math.min(out.width, bounds.width - ((bounds.left + bounds.width) - (pageXOffset + innerWidth))); 131 | out.width = Math.min(innerWidth, out.width); 132 | out.width -= 2; 133 | 134 | if (out.top < pageYOffset) { 135 | out.top = pageYOffset; 136 | } 137 | if (out.left < pageXOffset) { 138 | out.left = pageXOffset; 139 | } 140 | 141 | return out; 142 | } 143 | } 144 | 145 | export function removeUtilElements(body) { 146 | if (zeroElement) { 147 | body.removeChild(zeroElement); 148 | } 149 | zeroElement = null; 150 | } 151 | 152 | /** 153 | * Same as native getBoundingClientRect, except it takes into account parent offsets 154 | * if the element lies within a nested document ( or