🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Changelog
Please see [our GitHub "Releases" page](https://github.com/turbolinks/turbolinks/releases).
+20
View File
@@ -0,0 +1,20 @@
Copyright 2018 Basecamp, LLC
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+558
View File
@@ -0,0 +1,558 @@
# Turbolinks
**Turbolinks® makes navigating your web application faster.** Get the performance benefits of a single-page application without the added complexity of a client-side JavaScript framework. Use HTML to render your views on the server side and link to pages as usual. When you follow a link, Turbolinks automatically fetches the page, swaps in its `<body>`, and merges its `<head>`, all without incurring the cost of a full page load.
![Turbolinks](https://s3.amazonaws.com/turbolinks-docs/images/turbolinks.gif)
## Features
- **Optimizes navigation automatically.** No need to annotate links or specify which parts of the page should change.
- **No server-side cooperation necessary.** Respond with full HTML pages, not partial page fragments or JSON.
- **Respects the web.** The Back and Reload buttons work just as youd expect. Search engine-friendly by design.
- **Supports mobile apps.** Adapters for [iOS](https://github.com/turbolinks/turbolinks-ios) and [Android](https://github.com/turbolinks/turbolinks-android) let you build hybrid applications using native navigation controls.
## Supported Browsers
Turbolinks works in all modern desktop and mobile browsers. It depends on the [HTML5 History API](http://caniuse.com/#search=pushState) and [Window.requestAnimationFrame](http://caniuse.com/#search=requestAnimationFrame). In unsupported browsers, Turbolinks gracefully degrades to standard navigation.
## Installation
Include [`dist/turbolinks.js`](dist/turbolinks.js) in your applications JavaScript bundle.
Turbolinks automatically initializes itself when loaded via a standalone `<script>` tag or a traditional concatenated JavaScript bundle. If you load Turbolinks as a CommonJS or AMD module, first require the module, then call the provided `start()` function.
### Installation Using Ruby on Rails
Your Ruby on Rails application can use the [`turbolinks` RubyGem](https://github.com/turbolinks/turbolinks-rails) to install Turbolinks. This gem contains a Rails engine which integrates seamlessly with the Rails asset pipeline.
1. Add the `turbolinks` gem, version 5, to your Gemfile: `gem 'turbolinks', '~> 5.1.0'`
2. Run `bundle install`.
3. Add `//= require turbolinks` to your JavaScript manifest file (usually found at `app/assets/javascripts/application.js`).
The gem also provides server-side support for Turbolinks redirection, which can be used without the asset pipeline.
### Installation Using npm
Your application can use the [`turbolinks` npm package](https://www.npmjs.com/package/turbolinks) to install Turbolinks as a module for build tools like [webpack](http://webpack.github.io/).
1. Add the `turbolinks` package to your application: `npm install --save turbolinks`.
2. Require and start Turbolinks in your JavaScript bundle:
```js
var Turbolinks = require("turbolinks")
Turbolinks.start()
```
#### Table of Contents
[Navigating with Turbolinks](#navigating-with-turbolinks)
- [Each Navigation is a Visit](#each-navigation-is-a-visit)
- [Application Visits](#application-visits)
- [Restoration Visits](#restoration-visits)
- [Canceling Visits Before They Start](#canceling-visits-before-they-start)
- [Disabling Turbolinks on Specific Links](#disabling-turbolinks-on-specific-links)
[Building Your Turbolinks Application](#building-your-turbolinks-application)
- [Working with Script Elements](#working-with-script-elements)
- [Loading Your Applications JavaScript Bundle](#loading-your-applications-javascript-bundle)
- [Understanding Caching](#understanding-caching)
- [Preparing the Page to be Cached](#preparing-the-page-to-be-cached)
- [Detecting When a Preview is Visible](#detecting-when-a-preview-is-visible)
- [Opting Out of Caching](#opting-out-of-caching)
- [Installing JavaScript Behavior](#installing-javascript-behavior)
- [Observing Navigation Events](#observing-navigation-events)
- [Attaching Behavior With Stimulus](#attaching-behavior-with-stimulus)
- [Making Transformations Idempotent](#making-transformations-idempotent)
- [Persisting Elements Across Page Loads](#persisting-elements-across-page-loads)
[Advanced Usage](#advanced-usage)
- [Displaying Progress](#displaying-progress)
- [Reloading When Assets Change](#reloading-when-assets-change)
- [Ensuring Specific Pages Trigger a Full Reload](#ensuring-specific-pages-trigger-a-full-reload)
- [Setting a Root Location](#setting-a-root-location)
- [Following Redirects](#following-redirects)
- [Redirecting After a Form Submission](#redirecting-after-a-form-submission)
- [Setting Custom HTTP Headers](#setting-custom-http-headers)
[API Reference](#api-reference)
- [Turbolinks.visit](#turbolinksvisit)
- [Turbolinks.clearCache](#turbolinksclearcache)
- [Turbolinks.setProgressBarDelay](#turbolinkssetprogressbardelay)
- [Turbolinks.supported](#turbolinkssupported)
- [Full List of Events](#full-list-of-events)
[Contributing to Turbolinks](#contributing-to-turbolinks)
- [Building From Source](#building-from-source)
- [Running Tests](#running-tests)
# Navigating with Turbolinks
Turbolinks intercepts all clicks on `<a href>` links to the same domain. When you click an eligible link, Turbolinks prevents the browser from following it. Instead, Turbolinks changes the browsers URL using the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History), requests the new page using [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), and then renders the HTML response.
During rendering, Turbolinks replaces the current `<body>` element outright and merges the contents of the `<head>` element. The JavaScript `window` and `document` objects, and the HTML `<html>` element, persist from one rendering to the next.
## Each Navigation is a Visit
Turbolinks models navigation as a *visit* to a *location* (URL) with an *action*.
Visits represent the entire navigation lifecycle from click to render. That includes changing browser history, issuing the network request, restoring a copy of the page from cache, rendering the final response, and updating the scroll position.
There are two types of visit: an _application visit_, which has an action of _advance_ or _replace_, and a _restoration visit_, which has an action of _restore_.
## Application Visits
Application visits are initiated by clicking a Turbolinks-enabled link, or programmatically by calling [`Turbolinks.visit(location)`](#turbolinksvisit).
An application visit always issues a network request. When the response arrives, Turbolinks renders its HTML and completes the visit.
If possible, Turbolinks will render a preview of the page from cache immediately after the visit starts. This improves the perceived speed of frequent navigation between the same pages.
If the visits location includes an anchor, Turbolinks will attempt to scroll to the anchored element. Otherwise, it will scroll to the top of the page.
Application visits result in a change to the browsers history; the visits _action_ determines how.
![Advance visit action](https://s3.amazonaws.com/turbolinks-docs/images/advance.svg)
The default visit action is _advance_. During an advance visit, Turbolinks pushes a new entry onto the browsers history stack using [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState).
Applications using the Turbolinks [iOS adapter](https://github.com/turbolinks/turbolinks-ios) typically handle advance visits by pushing a new view controller onto the navigation stack. Similarly, applications using the [Android adapter](https://github.com/turbolinks/turbolinks-android) typically push a new activity onto the back stack.
![Replace visit action](https://s3.amazonaws.com/turbolinks-docs/images/replace.svg)
You may wish to visit a location without pushing a new history entry onto the stack. The _replace_ visit action uses [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to discard the topmost history entry and replace it with the new location.
To specify that following a link should trigger a replace visit, annotate the link with `data-turbolinks-action="replace"`:
```html
<a href="/edit" data-turbolinks-action="replace">Edit</a>
```
To programmatically visit a location with the replace action, pass the `action: "replace"` option to [`Turbolinks.visit`](#turbolinksvisit):
```js
Turbolinks.visit("/edit", { action: "replace" })
```
Applications using the Turbolinks [iOS adapter](https://github.com/turbolinks/turbolinks-ios) typically handle replace visits by dismissing the topmost view controller and pushing a new view controller onto the navigation stack without animation.
## Restoration Visits
Turbolinks automatically initiates a restoration visit when you navigate with the browsers Back or Forward buttons. Applications using the [iOS](https://github.com/turbolinks/turbolinks-ios) or [Android](https://github.com/turbolinks/turbolinks-android) adapters initiate a restoration visit when moving backward in the navigation stack.
![Restore visit action](https://s3.amazonaws.com/turbolinks-docs/images/restore.svg)
If possible, Turbolinks will render a copy of the page from cache without making a request. Otherwise, it will retrieve a fresh copy of the page over the network. See [Understanding Caching](#understanding-caching) for more details.
Turbolinks saves the scroll position of each page before navigating away and automatically returns to this saved position on restoration visits.
Restoration visits have an action of _restore_ and Turbolinks reserves them for internal use. You should not attempt to annotate links or invoke [`Turbolinks.visit`](#turbolinksvisit) with an action of `restore`.
## Canceling Visits Before They Start
Application visits can be canceled before they start, regardless of whether they were initiated by a link click or a call to [`Turbolinks.visit`](#turbolinksvisit).
Listen for the `turbolinks:before-visit` event to be notified when a visit is about to start, and use `event.data.url` (or `$event.originalEvent.data.url`, when using jQuery) to check the visits location. Then cancel the visit by calling `event.preventDefault()`.
Restoration visits cannot be canceled and do not fire `turbolinks:before-visit`. Turbolinks issues restoration visits in response to history navigation that has *already taken place*, typically via the browsers Back or Forward buttons.
## Disabling Turbolinks on Specific Links
Turbolinks can be disabled on a per-link basis by annotating a link or any of its ancestors with `data-turbolinks="false"`.
```html
<a href="/" data-turbolinks="false">Disabled</a>
<div data-turbolinks="false">
<a href="/">Disabled</a>
</div>
```
To reenable when an ancestor has opted out, use `data-turbolinks="true"`:
```html
<div data-turbolinks="false">
<a href="/" data-turbolinks="true">Enabled</a>
</div>
```
Links with Turbolinks disabled will be handled normally by the browser.
# Building Your Turbolinks Application
Turbolinks is fast because it doesnt reload the page when you follow a link. Instead, your application becomes a persistent, long-running process in the browser. This requires you to rethink the way you structure your JavaScript.
In particular, you can no longer depend on a full page load to reset your environment every time you navigate. The JavaScript `window` and `document` objects retain their state across page changes, and any other objects you leave in memory will stay in memory.
With awareness and a little extra care, you can design your application to gracefully handle this constraint without tightly coupling it to Turbolinks.
## Working with Script Elements
Your browser automatically loads and evaluates any `<script>` elements present on the initial page load.
When you navigate to a new page, Turbolinks looks for any `<script>` elements in the new pages `<head>` which arent present on the current page. Then it appends them to the current `<head>` where theyre loaded and evaluated by the browser. You can use this to load additional JavaScript files on-demand.
Turbolinks evaluates `<script>` elements in a pages `<body>` each time it renders the page. You can use inline body scripts to set up per-page JavaScript state or bootstrap client-side models. To install behavior, or to perform more complex operations when the page changes, avoid script elements and use the `turbolinks:load` event instead.
Annotate `<script>` elements with `data-turbolinks-eval="false"` if you do not want Turbolinks to evaluate them after rendering. Note that this annotation will not prevent your browser from evaluating scripts on the initial page load.
### Loading Your Applications JavaScript Bundle
Always make sure to load your applications JavaScript bundle using `<script>` elements in the `<head>` of your document. Otherwise, Turbolinks will reload the bundle with every page change.
```html
<head>
...
<script src="/application-cbd3cd4.js" defer></script>
</head>
```
If you have traditionally placed application scripts at the end of `<body>` for performance reasons, consider using the [`<script defer>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer) attribute instead. It has [widespread browser support](https://caniuse.com/#feat=script-defer) and allows you to keep your scripts in `<head>` for Turbolinks compatibility.
You should also consider configuring your asset packaging system to fingerprint each script so it has a new URL when its contents change. Then you can use the `data-turbolinks-track` attribute to force a full page reload when you deploy a new JavaScript bundle. See [Reloading When Assets Change](#reloading-when-assets-change) for information.
## Understanding Caching
Turbolinks maintains a cache of recently visited pages. This cache serves two purposes: to display pages without accessing the network during restoration visits, and to improve perceived performance by showing temporary previews during application visits.
When navigating by history (via [Restoration Visits](#restoration-visits)), Turbolinks will restore the page from cache without loading a fresh copy from the network, if possible.
Otherwise, during standard navigation (via [Application Visits](#application-visits)), Turbolinks will immediately restore the page from cache and display it as a preview while simultaneously loading a fresh copy from the network. This gives the illusion of instantaneous page loads for frequently accessed locations.
Turbolinks saves a copy of the current page to its cache just before rendering a new page. Note that Turbolinks copies the page using [`cloneNode(true)`](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode), which means any attached event listeners and associated data are discarded.
### Preparing the Page to be Cached
Listen for the `turbolinks:before-cache` event if you need to prepare the document before Turbolinks caches it. You can use this event to reset forms, collapse expanded UI elements, or tear down any third-party widgets so the page is ready to be displayed again.
```js
document.addEventListener("turbolinks:before-cache", function() {
// ...
})
```
### Detecting When a Preview is Visible
Turbolinks adds a `data-turbolinks-preview` attribute to the `<html>` element when it displays a preview from cache. You can check for the presence of this attribute to selectively enable or disable behavior when a preview is visible.
```js
if (document.documentElement.hasAttribute("data-turbolinks-preview")) {
// Turbolinks is displaying a preview
}
```
### Opting Out of Caching
You can control caching behavior on a per-page basis by including a `<meta name="turbolinks-cache-control">` element in your pages `<head>` and declaring a caching directive.
Use the `no-preview` directive to specify that a cached version of the page should not be shown as a preview during an application visit. Pages marked no-preview will only be used for restoration visits.
To specify that a page should not be cached at all, use the `no-cache` directive. Pages marked no-cache will always be fetched over the network, including during restoration visits.
```html
<head>
...
<meta name="turbolinks-cache-control" content="no-cache">
</head>
```
To completely disable caching in your application, ensure every page contains a no-cache directive.
## Installing JavaScript Behavior
You may be used to installing JavaScript behavior in response to the `window.onload`, `DOMContentLoaded`, or jQuery `ready` events. With Turbolinks, these events will fire only in response to the initial page load, not after any subsequent page changes. We compare two strategies for connecting JavaScript behavior to the DOM below.
### Observing Navigation Events
Turbolinks triggers a series of events during navigation. The most significant of these is the `turbolinks:load` event, which fires once on the initial page load, and again after every Turbolinks visit.
You can observe the `turbolinks:load` event in place of `DOMContentLoaded` to set up JavaScript behavior after every page change:
```js
document.addEventListener("turbolinks:load", function() {
// ...
})
```
Keep in mind that your application will not always be in a pristine state when this event is fired, and you may need to clean up behavior installed for the previous page.
Also note that Turbolinks navigation may not be the only source of page updates in your application, so you may wish to move your initialization code into a separate function which you can call from `turbolinks:load` and anywhere else you may change the DOM.
When possible, avoid using the `turbolinks:load` event to add other event listeners directly to elements on the page body. Instead, consider using [event delegation](https://learn.jquery.com/events/event-delegation/) to register event listeners once on `document` or `window`.
See the [Full List of Events](#full-list-of-events) for more information.
### Attaching Behavior With Stimulus
New DOM elements can appear on the page at any time by way of Ajax request handlers, WebSocket handlers, or client-side rendering operations, and these elements often need to be initialized as if they came from a fresh page load.
You can handle all of these updates, including updates from Turbolinks page loads, in a single place with the conventions and lifecycle callbacks provided by Turbolinks sister framework, [Stimulus](https://github.com/stimulusjs/stimulus).
Stimulus lets you annotate your HTML with controller, action, and target attributes:
```html
<div data-controller="hello">
<input data-target="hello.name" type="text">
<button data-action="click->hello#greet">Greet</button>
</div>
```
Implement a compatible controller and Stimulus connects it automatically:
```js
// hello_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
greet() {
console.log(`Hello, ${this.name}!`)
}
get name() {
return this.targets.find("name").value
}
}
```
Stimulus connects and disconnects these controllers and their associated event handlers whenever the document changes using the [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) API. As a result, it handles Turbolinks page changes the same way it handles any other type of DOM update.
See the [Stimulus repository on GitHub](https://github.com/stimulusjs/stimulus) for more information.
## Making Transformations Idempotent
Often youll want to perform client-side transformations to HTML received from the server. For example, you might want to use the browsers knowledge of the users current time zone to group a collection of elements by date.
Suppose you have annotated a set of elements with `data-timestamp` attributes indicating the elements creation times in UTC. You have a JavaScript function that queries the document for all such elements, converts the timestamps to local time, and inserts date headers before each element that occurs on a new day.
Consider what happens if youve configured this function to run on `turbolinks:load`. When you navigate to the page, your function inserts date headers. Navigate away, and Turbolinks saves a copy of the transformed page to its cache. Now press the Back button—Turbolinks restores the page, fires `turbolinks:load` again, and your function inserts a second set of date headers.
To avoid this problem, make your transformation function _idempotent_. An idempotent transformation is safe to apply multiple times without changing the result beyond its initial application.
One technique for making a transformation idempotent is to keep track of whether youve already performed it by setting a `data` attribute on each processed element. When Turbolinks restores your page from cache, these attributes will still be present. Detect these attributes in your transformation function to determine which elements have already been processed.
A more robust technique is simply to detect the transformation itself. In the date grouping example above, that means checking for the presence of a date divider before inserting a new one. This approach gracefully handles newly inserted elements that werent processed by the original transformation.
## Persisting Elements Across Page Loads
Turbolinks allows you to mark certain elements as _permanent_. Permanent elements persist across page loads, so that any changes you make to those elements do not need to be reapplied after navigation.
Consider a Turbolinks application with a shopping cart. At the top of each page is an icon with the number of items currently in the cart. This counter is updated dynamically with JavaScript as items are added and removed.
Now imagine a user who has navigated to several pages in this application. She adds an item to her cart, then presses the Back button in her browser. Upon navigation, Turbolinks restores the previous pages state from cache, and the cart item count erroneously changes from 1 to 0.
You can avoid this problem by marking the counter element as permanent. Designate permanent elements by giving them an HTML `id` and annotating them with `data-turbolinks-permanent`.
```html
<div id="cart-counter" data-turbolinks-permanent>1 item</div>
```
Before each render, Turbolinks matches all permanent elements by `id` and transfers them from the original page to the new page, preserving their data and event listeners.
# Advanced Usage
## Displaying Progress
During Turbolinks navigation, the browser will not display its native progress indicator. Turbolinks installs a CSS-based progress bar to provide feedback while issuing a request.
The progress bar is enabled by default. It appears automatically for any page that takes longer than 500ms to load. (You can change this delay with the [`Turbolinks.setProgressBarDelay`](#turbolinkssetprogressbardelay) method.)
The progress bar is a `<div>` element with the class name `turbolinks-progress-bar`. Its default styles appear first in the document and can be overridden by rules that come later.
For example, the following CSS will result in a thick green progress bar:
```css
.turbolinks-progress-bar {
height: 5px;
background-color: green;
}
```
To disable the progress bar entirely, set its `visibility` style to `hidden`:
```css
.turbolinks-progress-bar {
visibility: hidden;
}
```
## Reloading When Assets Change
Turbolinks can track the URLs of asset elements in `<head>` from one page to the next and automatically issue a full reload if they change. This ensures that users always have the latest versions of your applications scripts and styles.
Annotate asset elements with `data-turbolinks-track="reload"` and include a version identifier in your asset URLs. The identifier could be a number, a last-modified timestamp, or better, a digest of the assets contents, as in the following example.
```html
<head>
...
<link rel="stylesheet" href="/application-258e88d.css" data-turbolinks-track="reload">
<script src="/application-cbd3cd4.js" data-turbolinks-track="reload"></script>
</head>
```
## Ensuring Specific Pages Trigger a Full Reload
You can ensure visits to a certain page will always trigger a full reload by including a `<meta name="turbolinks-visit-control">` element in the pages `<head>`.
```html
<head>
...
<meta name="turbolinks-visit-control" content="reload">
</head>
```
This setting may be useful as a workaround for third-party JavaScript libraries that dont interact well with Turbolinks page changes.
## Setting a Root Location
By default, Turbolinks only loads URLs with the same origin—i.e. the same protocol, domain name, and port—as the current document. A visit to any other URL falls back to a full page load.
In some cases, you may want to further scope Turbolinks to a path on the same origin. For example, if your Turbolinks application lives at `/app`, and the non-Turbolinks help site lives at `/help`, links from the app to the help site shouldnt use Turbolinks.
Include a `<meta name="turbolinks-root">` element in your pages `<head>` to scope Turbolinks to a particular root location. Turbolinks will only load same-origin URLs that are prefixed with this path.
```html
<head>
...
<meta name="turbolinks-root" content="/app">
</head>
```
## Following Redirects
When you visit location `/one` and the server redirects you to location `/two`, you expect the browsers address bar to display the redirected URL.
However, Turbolinks makes requests using `XMLHttpRequest`, which transparently follows redirects. Theres no way for Turbolinks to tell whether a request resulted in a redirect without additional cooperation from the server.
To work around this problem, send the `Turbolinks-Location` header in response to a visit that was redirected, and Turbolinks will replace the browsers topmost history entry with the value you provide.
The Turbolinks Rails engine sets `Turbolinks-Location` automatically when using `redirect_to` in response to a Turbolinks visit.
## Redirecting After a Form Submission
Submitting an HTML form to the server and redirecting in response is a common pattern in web applications. Standard form submission is similar to navigation, resulting in a full page load. Using Turbolinks you can improve the performance of form submission without complicating your server-side code.
Instead of submitting forms normally, submit them with XHR. In response to an XHR submit on the server, return JavaScript that performs a [`Turbolinks.visit`](#turbolinksvisit) to be evaluated by the browser.
If form submission results in a state change on the server that affects cached pages, consider clearing Turbolinks cache with [`Turbolinks.clearCache()`](#turbolinksclearcache).
The Turbolinks Rails engine performs this optimization automatically for non-GET XHR requests that redirect with the `redirect_to` helper.
## Setting Custom HTTP Headers
You can observe the `turbolinks:request-start` event to set custom headers on Turbolinks requests. Access the requests XMLHttpRequest object via `event.data.xhr`, then call the [`setRequestHeader`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) method as many times as you wish.
For example, you might want to include a request ID with every Turbolinks link click and programmatic visit.
```javascript
document.addEventListener("turbolinks:request-start", function(event) {
var xhr = event.data.xhr
xhr.setRequestHeader("X-Request-Id", "123...")
})
```
# API Reference
## Turbolinks.visit
Usage:
```js
Turbolinks.visit(location)
Turbolinks.visit(location, { action: action })
```
Performs an [Application Visit](#application-visits) to the given _location_ (a string containing a URL or path) with the specified _action_ (a string, either `"advance"` or `"replace"`).
If _location_ is a cross-origin URL, or falls outside of the specified root (see [Setting a Root Location](#setting-a-root-location)), or if the value of [`Turbolinks.supported`](#turbolinkssupported) is `false`, Turbolinks performs a full page load by setting `window.location`.
If _action_ is unspecified, Turbolinks assumes a value of `"advance"`.
Before performing the visit, Turbolinks fires a `turbolinks:before-visit` event on `document`. Your application can listen for this event and cancel the visit with `event.preventDefault()` (see [Canceling Visits Before They Start](#canceling-visits-before-they-start)).
## Turbolinks.clearCache
Usage:
```js
Turbolinks.clearCache()
```
Removes all entries from the Turbolinks page cache. Call this when state has changed on the server that may affect cached pages.
## Turbolinks.setProgressBarDelay
Usage:
```js
Turbolinks.setProgressBarDelay(delayInMilliseconds)
```
Sets the delay after which the [progress bar](#displaying-progress) will appear during navigation, in milliseconds. The progress bar appears after 500ms by default.
Note that this method has no effect when used with the iOS or Android adapters.
## Turbolinks.supported
Usage:
```js
if (Turbolinks.supported) {
// ...
}
```
Detects whether Turbolinks is supported in the current browser (see [Supported Browsers](#supported-browsers)).
## Full List of Events
Turbolinks emits events that allow you to track the navigation lifecycle and respond to page loading. Except where noted, Turbolinks fires events on the `document` object.
* `turbolinks:click` fires when you click a Turbolinks-enabled link. The clicked element is the event target. Access the requested location with `event.data.url`. Cancel this event to let the click fall through to the browser as normal navigation.
* `turbolinks:before-visit` fires before visiting a location, except when navigating by history. Access the requested location with `event.data.url`. Cancel this event to prevent navigation.
* `turbolinks:visit` fires immediately after a visit starts.
* `turbolinks:request-start` fires before Turbolinks issues a network request to fetch the page. Access the XMLHttpRequest object with `event.data.xhr`.
* `turbolinks:request-end` fires after the network request completes. Access the XMLHttpRequest object with `event.data.xhr`.
* `turbolinks:before-cache` fires before Turbolinks saves the current page to cache.
* `turbolinks:before-render` fires before rendering the page. Access the new `<body>` element with `event.data.newBody`.
* `turbolinks:render` fires after Turbolinks renders the page. This event fires twice during an application visit to a cached location: once after rendering the cached version, and again after rendering the fresh version.
* `turbolinks:load` fires once after the initial page load, and again after every Turbolinks visit. Access visit timing metrics with the `event.data.timing` object.
# Contributing to Turbolinks
Turbolinks is open-source software, freely distributable under the terms of an [MIT-style license](LICENSE). The [source code is hosted on GitHub](https://github.com/turbolinks/turbolinks).
Development is sponsored by [Basecamp](https://basecamp.com/).
We welcome contributions in the form of bug reports, pull requests, or thoughtful discussions in the [GitHub issue tracker](https://github.com/turbolinks/turbolinks/issues).
Please note that this project is released with a [Contributor Code of Conduct](CONDUCT.md). By participating in this project you agree to abide by its terms.
## Building From Source
Turbolinks is written in [CoffeeScript](https://github.com/jashkenas/coffee-script) and compiled to JavaScript with [Blade](https://github.com/javan/blade). To build from source youll need a recent version of Ruby. From the root of your Turbolinks directory, issue the following commands to build the distributable files in `dist/`:
```
$ gem install bundler
$ bundle install
$ bin/blade build
```
## Running Tests
The Turbolinks test suite is written in [TypeScript](https://www.typescriptlang.org) with the [Intern testing library](https://theintern.io).
To run the tests, first make sure you have the [Yarn package manager](https://yarnpkg.com) installed. Follow the instructions for _Building From Source_ above, then run the following commands:
```
$ cd test
$ yarn install
$ yarn test
```
If you are testing changes to the Turbolinks source, remember to run `bin/blade build` before each test run.
---
© 2018 Basecamp, LLC.
File diff suppressed because one or more lines are too long
+28
View File
@@ -0,0 +1,28 @@
{
"name": "turbolinks",
"version": "5.2.0",
"description": "Turbolinks makes navigating your web application faster",
"main": "dist/turbolinks.js",
"files": [
"dist",
"src"
],
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/turbolinks/turbolinks.git"
},
"keywords": [
"turbolinks",
"browser",
"pushstate"
],
"author": "javan, packagethief, sstephenson",
"license": "MIT",
"bugs": {
"url": "https://github.com/turbolinks/turbolinks/issues"
},
"homepage": "https://github.com/turbolinks/turbolinks#readme"
}
@@ -0,0 +1,4 @@
/*
Turbolinks <%= depend_on_asset("./VERSION").to_s.strip %>
Copyright © <%= Time.now.year %> Basecamp, LLC
*/
@@ -0,0 +1 @@
5.2.0
@@ -0,0 +1,60 @@
#= require ./http_request
#= require ./progress_bar
class Turbolinks.BrowserAdapter
{NETWORK_FAILURE, TIMEOUT_FAILURE} = Turbolinks.HttpRequest
constructor: (@controller) ->
@progressBar = new Turbolinks.ProgressBar
visitProposedToLocationWithAction: (location, action) ->
@controller.startVisitToLocationWithAction(location, action)
visitStarted: (visit) ->
visit.issueRequest()
visit.changeHistory()
visit.loadCachedSnapshot()
visitRequestStarted: (visit) ->
@progressBar.setValue(0)
if visit.hasCachedSnapshot() or visit.action isnt "restore"
@showProgressBarAfterDelay()
else
@showProgressBar()
visitRequestProgressed: (visit) ->
@progressBar.setValue(visit.progress)
visitRequestCompleted: (visit) ->
visit.loadResponse()
visitRequestFailedWithStatusCode: (visit, statusCode) ->
switch statusCode
when NETWORK_FAILURE, TIMEOUT_FAILURE
@reload()
else
visit.loadResponse()
visitRequestFinished: (visit) ->
@hideProgressBar()
visitCompleted: (visit) ->
visit.followRedirect()
pageInvalidated: ->
@reload()
# Private
showProgressBarAfterDelay: ->
@progressBarTimeout = setTimeout(@showProgressBar, @controller.progressBarDelay)
showProgressBar: =>
@progressBar.show()
hideProgressBar: ->
@progressBar.hide()
clearTimeout(@progressBarTimeout)
reload: ->
window.location.reload()
@@ -0,0 +1,30 @@
{defer, dispatch} = Turbolinks
handleEvent = (eventName, handler) ->
document.addEventListener(eventName, handler, false)
translateEvent = ({from, to}) ->
handler = (event) ->
event = dispatch(to, target: event.target, cancelable: event.cancelable, data: event.data)
event.preventDefault() if event.defaultPrevented
handleEvent(from, handler)
translateEvent from: "turbolinks:click", to: "page:before-change"
translateEvent from: "turbolinks:request-start", to: "page:fetch"
translateEvent from: "turbolinks:request-end", to: "page:receive"
translateEvent from: "turbolinks:before-cache", to: "page:before-unload"
translateEvent from: "turbolinks:render", to: "page:update"
translateEvent from: "turbolinks:load", to: "page:change"
translateEvent from: "turbolinks:load", to: "page:update"
loaded = false
handleEvent "DOMContentLoaded", ->
defer ->
loaded = true
handleEvent "turbolinks:load", ->
if loaded
dispatch("page:load")
jQuery?(document).on "ajaxSuccess", (event, xhr, settings) ->
if jQuery.trim(xhr.responseText).length > 0
dispatch("page:update")
@@ -0,0 +1,244 @@
#= require ./location
#= require ./browser_adapter
#= require ./history
#= require ./view
#= require ./scroll_manager
#= require ./snapshot_cache
#= require ./visit
class Turbolinks.Controller
constructor: ->
@history = new Turbolinks.History this
@view = new Turbolinks.View this
@scrollManager = new Turbolinks.ScrollManager this
@restorationData = {}
@clearCache()
@setProgressBarDelay(500)
start: ->
if Turbolinks.supported and not @started
addEventListener("click", @clickCaptured, true)
addEventListener("DOMContentLoaded", @pageLoaded, false)
@scrollManager.start()
@startHistory()
@started = true
@enabled = true
disable: ->
@enabled = false
stop: ->
if @started
removeEventListener("click", @clickCaptured, true)
removeEventListener("DOMContentLoaded", @pageLoaded, false)
@scrollManager.stop()
@stopHistory()
@started = false
clearCache: ->
@cache = new Turbolinks.SnapshotCache 10
visit: (location, options = {}) ->
location = Turbolinks.Location.wrap(location)
if @applicationAllowsVisitingLocation(location)
if @locationIsVisitable(location)
action = options.action ? "advance"
@adapter.visitProposedToLocationWithAction(location, action)
else
window.location = location
startVisitToLocationWithAction: (location, action, restorationIdentifier) ->
if Turbolinks.supported
restorationData = @getRestorationDataForIdentifier(restorationIdentifier)
@startVisit(location, action, {restorationData})
else
window.location = location
setProgressBarDelay: (delay) ->
@progressBarDelay = delay
# History
startHistory: ->
@location = Turbolinks.Location.wrap(window.location)
@restorationIdentifier = Turbolinks.uuid()
@history.start()
@history.replace(@location, @restorationIdentifier)
stopHistory: ->
@history.stop()
pushHistoryWithLocationAndRestorationIdentifier: (location, @restorationIdentifier) ->
@location = Turbolinks.Location.wrap(location)
@history.push(@location, @restorationIdentifier)
replaceHistoryWithLocationAndRestorationIdentifier: (location, @restorationIdentifier) ->
@location = Turbolinks.Location.wrap(location)
@history.replace(@location, @restorationIdentifier)
# History delegate
historyPoppedToLocationWithRestorationIdentifier: (location, @restorationIdentifier) ->
if @enabled
restorationData = @getRestorationDataForIdentifier(@restorationIdentifier)
@startVisit(location, "restore", {@restorationIdentifier, restorationData, historyChanged: true})
@location = Turbolinks.Location.wrap(location)
else
@adapter.pageInvalidated()
# Snapshot cache
getCachedSnapshotForLocation: (location) ->
@cache.get(location)?.clone()
shouldCacheSnapshot: ->
@view.getSnapshot().isCacheable()
cacheSnapshot: ->
if @shouldCacheSnapshot()
@notifyApplicationBeforeCachingSnapshot()
snapshot = @view.getSnapshot()
location = @lastRenderedLocation
Turbolinks.defer =>
@cache.put(location, snapshot.clone())
# Scrolling
scrollToAnchor: (anchor) ->
if element = @view.getElementForAnchor(anchor)
@scrollToElement(element)
else
@scrollToPosition(x: 0, y: 0)
scrollToElement: (element) ->
@scrollManager.scrollToElement(element)
scrollToPosition: (position) ->
@scrollManager.scrollToPosition(position)
# Scroll manager delegate
scrollPositionChanged: (scrollPosition) ->
restorationData = @getCurrentRestorationData()
restorationData.scrollPosition = scrollPosition
# View
render: (options, callback) ->
@view.render(options, callback)
viewInvalidated: ->
@adapter.pageInvalidated()
viewWillRender: (newBody) ->
@notifyApplicationBeforeRender(newBody)
viewRendered: ->
@lastRenderedLocation = @currentVisit.location
@notifyApplicationAfterRender()
# Event handlers
pageLoaded: =>
@lastRenderedLocation = @location
@notifyApplicationAfterPageLoad()
clickCaptured: =>
removeEventListener("click", @clickBubbled, false)
addEventListener("click", @clickBubbled, false)
clickBubbled: (event) =>
if @enabled and @clickEventIsSignificant(event)
if link = @getVisitableLinkForNode(event.target)
if location = @getVisitableLocationForLink(link)
if @applicationAllowsFollowingLinkToLocation(link, location)
event.preventDefault()
action = @getActionForLink(link)
@visit(location, {action})
# Application events
applicationAllowsFollowingLinkToLocation: (link, location) ->
event = @notifyApplicationAfterClickingLinkToLocation(link, location)
not event.defaultPrevented
applicationAllowsVisitingLocation: (location) ->
event = @notifyApplicationBeforeVisitingLocation(location)
not event.defaultPrevented
notifyApplicationAfterClickingLinkToLocation: (link, location) ->
Turbolinks.dispatch("turbolinks:click", target: link, data: { url: location.absoluteURL }, cancelable: true)
notifyApplicationBeforeVisitingLocation: (location) ->
Turbolinks.dispatch("turbolinks:before-visit", data: { url: location.absoluteURL }, cancelable: true)
notifyApplicationAfterVisitingLocation: (location) ->
Turbolinks.dispatch("turbolinks:visit", data: { url: location.absoluteURL })
notifyApplicationBeforeCachingSnapshot: ->
Turbolinks.dispatch("turbolinks:before-cache")
notifyApplicationBeforeRender: (newBody) ->
Turbolinks.dispatch("turbolinks:before-render", data: {newBody})
notifyApplicationAfterRender: ->
Turbolinks.dispatch("turbolinks:render")
notifyApplicationAfterPageLoad: (timing = {}) ->
Turbolinks.dispatch("turbolinks:load", data: { url: @location.absoluteURL, timing })
# Private
startVisit: (location, action, properties) ->
@currentVisit?.cancel()
@currentVisit = @createVisit(location, action, properties)
@currentVisit.start()
@notifyApplicationAfterVisitingLocation(location)
createVisit: (location, action, {restorationIdentifier, restorationData, historyChanged} = {}) ->
visit = new Turbolinks.Visit this, location, action
visit.restorationIdentifier = restorationIdentifier ? Turbolinks.uuid()
visit.restorationData = Turbolinks.copyObject(restorationData)
visit.historyChanged = historyChanged
visit.referrer = @location
visit
visitCompleted: (visit) ->
@notifyApplicationAfterPageLoad(visit.getTimingMetrics())
clickEventIsSignificant: (event) ->
not (
event.defaultPrevented or
event.target.isContentEditable or
event.which > 1 or
event.altKey or
event.ctrlKey or
event.metaKey or
event.shiftKey
)
getVisitableLinkForNode: (node) ->
if @nodeIsVisitable(node)
Turbolinks.closest(node, "a[href]:not([target]):not([download])")
getVisitableLocationForLink: (link) ->
location = new Turbolinks.Location link.getAttribute("href")
location if @locationIsVisitable(location)
getActionForLink: (link) ->
link.getAttribute("data-turbolinks-action") ? "advance"
nodeIsVisitable: (node) ->
if container = Turbolinks.closest(node, "[data-turbolinks]")
container.getAttribute("data-turbolinks") isnt "false"
else
true
locationIsVisitable: (location) ->
location.isPrefixedBy(@view.getRootLocation()) and location.isHTML()
getCurrentRestorationData: ->
@getRestorationDataForIdentifier(@restorationIdentifier)
getRestorationDataForIdentifier: (identifier) ->
@restorationData[identifier] ?= {}
@@ -0,0 +1,27 @@
#= require ./renderer
class Turbolinks.ErrorRenderer extends Turbolinks.Renderer
constructor: (html) ->
htmlElement = document.createElement("html")
htmlElement.innerHTML = html
@newHead = htmlElement.querySelector("head")
@newBody = htmlElement.querySelector("body")
render: (callback) ->
@renderView =>
@replaceHeadAndBody()
@activateBodyScriptElements()
callback()
replaceHeadAndBody: ->
{head, body} = document
head.parentNode.replaceChild(@newHead, head)
body.parentNode.replaceChild(@newBody, body)
activateBodyScriptElements: ->
for replaceableElement in @getScriptElements()
element = @createScriptElement(replaceableElement)
replaceableElement.parentNode.replaceChild(element, replaceableElement)
getScriptElements: ->
document.documentElement.querySelectorAll("script")
@@ -0,0 +1,68 @@
class Turbolinks.HeadDetails
@fromHeadElement: (headElement) ->
new this headElement?.childNodes ? []
constructor: (childNodes) ->
@elements = {}
for node in childNodes when node.nodeType is Node.ELEMENT_NODE
key = node.outerHTML
data = @elements[key] ?=
type: elementType(node)
tracked: elementIsTracked(node)
elements: []
data.elements.push(node)
hasElementWithKey: (key) ->
key of @elements
getTrackedElementSignature: ->
(key for key, {tracked} of @elements when tracked).join("")
getScriptElementsNotInDetails: (headDetails) ->
@getElementsMatchingTypeNotInDetails("script", headDetails)
getStylesheetElementsNotInDetails: (headDetails) ->
@getElementsMatchingTypeNotInDetails("stylesheet", headDetails)
getElementsMatchingTypeNotInDetails: (matchedType, headDetails) ->
elements[0] for key, {type, elements} of @elements when type is matchedType and not headDetails.hasElementWithKey(key)
getProvisionalElements: ->
provisionalElements = []
for key, {type, tracked, elements} of @elements
if not type? and not tracked
provisionalElements.push(elements...)
else if elements.length > 1
provisionalElements.push(elements[1...]...)
provisionalElements
getMetaValue: (name) ->
@findMetaElementByName(name)?.getAttribute("content")
findMetaElementByName: (name) ->
element = undefined
for key, {elements} of @elements
if elementIsMetaElementWithName(elements[0], name)
element = elements[0]
element
elementType = (element) ->
if elementIsScript(element)
"script"
else if elementIsStylesheet(element)
"stylesheet"
elementIsTracked = (element) ->
element.getAttribute("data-turbolinks-track") is "reload"
elementIsScript = (element) ->
tagName = element.tagName.toLowerCase()
tagName is "script"
elementIsStylesheet = (element) ->
tagName = element.tagName.toLowerCase()
tagName is "style" or (tagName is "link" and element.getAttribute("rel") is "stylesheet")
elementIsMetaElementWithName = (element, name) ->
tagName = element.tagName.toLowerCase()
tagName is "meta" and element.getAttribute("name") is name
@@ -0,0 +1,74 @@
Turbolinks.copyObject = (object) ->
result = {}
for key, value of object
result[key] = value
result
Turbolinks.closest = (element, selector) ->
closest.call(element, selector)
closest = do ->
html = document.documentElement
html.closest ? (selector) ->
node = this
while node
return node if node.nodeType is Node.ELEMENT_NODE and match.call(node, selector)
node = node.parentNode
Turbolinks.defer = (callback) ->
setTimeout(callback, 1)
Turbolinks.throttle = (fn) ->
request = null
(args...) ->
request ?= requestAnimationFrame =>
request = null
fn.apply(this, args)
Turbolinks.dispatch = (eventName, {target, cancelable, data} = {}) ->
event = document.createEvent("Events")
event.initEvent(eventName, true, cancelable is true)
event.data = data ? {}
# Fix setting `defaultPrevented` when `preventDefault()` is called
# http://stackoverflow.com/questions/23349191/event-preventdefault-is-not-working-in-ie-11-for-custom-events
if event.cancelable and not preventDefaultSupported
{ preventDefault } = event
event.preventDefault = ->
unless this.defaultPrevented
Object.defineProperty(this, "defaultPrevented", get: -> true)
preventDefault.call(this)
(target ? document).dispatchEvent(event)
event
preventDefaultSupported = do ->
event = document.createEvent("Events")
event.initEvent("test", true, true)
event.preventDefault()
event.defaultPrevented
Turbolinks.match = (element, selector) ->
match.call(element, selector)
match = do ->
html = document.documentElement
html.matchesSelector ? html.webkitMatchesSelector ? html.msMatchesSelector ? html.mozMatchesSelector
Turbolinks.uuid = ->
result = ""
for i in [1..36]
if i in [9, 14, 19, 24]
result += "-"
else if i is 15
result += "4"
else if i is 20
result += (Math.floor(Math.random() * 4) + 8).toString(16)
else
result += Math.floor(Math.random() * 15).toString(16)
result
@@ -0,0 +1,48 @@
class Turbolinks.History
constructor: (@delegate) ->
start: ->
unless @started
addEventListener("popstate", @onPopState, false)
addEventListener("load", @onPageLoad, false)
@started = true
stop: ->
if @started
removeEventListener("popstate", @onPopState, false)
removeEventListener("load", @onPageLoad, false)
@started = false
push: (location, restorationIdentifier) ->
location = Turbolinks.Location.wrap(location)
@update("push", location, restorationIdentifier)
replace: (location, restorationIdentifier) ->
location = Turbolinks.Location.wrap(location)
@update("replace", location, restorationIdentifier)
# Event handlers
onPopState: (event) =>
if @shouldHandlePopState()
if turbolinks = event.state?.turbolinks
location = Turbolinks.Location.wrap(window.location)
restorationIdentifier = turbolinks.restorationIdentifier
@delegate.historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier)
onPageLoad: (event) =>
Turbolinks.defer =>
@pageLoaded = true
# Private
shouldHandlePopState: ->
# Safari dispatches a popstate event after window's load event, ignore it
@pageIsLoaded()
pageIsLoaded: ->
@pageLoaded or document.readyState is "complete"
update: (method, location, restorationIdentifier) ->
state = turbolinks: {restorationIdentifier}
history[method + "State"](state, null, location)
@@ -0,0 +1,88 @@
class Turbolinks.HttpRequest
@NETWORK_FAILURE = 0
@TIMEOUT_FAILURE = -1
@timeout = 60
constructor: (@delegate, location, referrer) ->
@url = Turbolinks.Location.wrap(location).requestURL
@referrer = Turbolinks.Location.wrap(referrer).absoluteURL
@createXHR()
send: ->
if @xhr and not @sent
@notifyApplicationBeforeRequestStart()
@setProgress(0)
@xhr.send()
@sent = true
@delegate.requestStarted?()
cancel: ->
if @xhr and @sent
@xhr.abort()
# XMLHttpRequest events
requestProgressed: (event) =>
if event.lengthComputable
@setProgress(event.loaded / event.total)
requestLoaded: =>
@endRequest =>
if 200 <= @xhr.status < 300
@delegate.requestCompletedWithResponse(@xhr.responseText, @xhr.getResponseHeader("Turbolinks-Location"))
else
@failed = true
@delegate.requestFailedWithStatusCode(@xhr.status, @xhr.responseText)
requestFailed: =>
@endRequest =>
@failed = true
@delegate.requestFailedWithStatusCode(@constructor.NETWORK_FAILURE)
requestTimedOut: =>
@endRequest =>
@failed = true
@delegate.requestFailedWithStatusCode(@constructor.TIMEOUT_FAILURE)
requestCanceled: =>
@endRequest()
# Application events
notifyApplicationBeforeRequestStart: ->
Turbolinks.dispatch("turbolinks:request-start", data: { url: @url, xhr: @xhr })
notifyApplicationAfterRequestEnd: ->
Turbolinks.dispatch("turbolinks:request-end", data: { url: @url, xhr: @xhr })
# Private
createXHR: ->
@xhr = new XMLHttpRequest
@xhr.open("GET", @url, true)
@xhr.timeout = @constructor.timeout * 1000
@xhr.setRequestHeader("Accept", "text/html, application/xhtml+xml")
@xhr.setRequestHeader("Turbolinks-Referrer", @referrer)
@xhr.onprogress = @requestProgressed
@xhr.onload = @requestLoaded
@xhr.onerror = @requestFailed
@xhr.ontimeout = @requestTimedOut
@xhr.onabort = @requestCanceled
endRequest: (callback) ->
if @xhr
@notifyApplicationAfterRequestEnd()
callback?.call(this)
@destroy()
setProgress: (progress) ->
@progress = progress
@delegate.requestProgressed?(@progress)
destroy: ->
@setProgress(1)
@delegate.requestFinished?()
@delegate = null
@xhr = null
@@ -0,0 +1,22 @@
#= require ./BANNER
#= export Turbolinks
#= require_self
#= require ./helpers
#= require ./controller
#= require ./script_warning
#= require ./start
@Turbolinks =
supported: do ->
window.history.pushState? and
window.requestAnimationFrame? and
window.addEventListener?
visit: (location, options) ->
Turbolinks.controller.visit(location, options)
clearCache: ->
Turbolinks.controller.clearCache()
setProgressBarDelay: (delay) ->
Turbolinks.controller.setProgressBarDelay(delay)
@@ -0,0 +1,70 @@
class Turbolinks.Location
@wrap: (value) ->
if value instanceof this
value
else
new this value
constructor: (url = "") ->
linkWithAnchor = document.createElement("a")
linkWithAnchor.href = url.toString()
@absoluteURL = linkWithAnchor.href
anchorLength = linkWithAnchor.hash.length
if anchorLength < 2
@requestURL = @absoluteURL
else
@requestURL = @absoluteURL.slice(0, -anchorLength)
@anchor = linkWithAnchor.hash.slice(1)
getOrigin: ->
@absoluteURL.split("/", 3).join("/")
getPath: ->
@requestURL.match(/\/\/[^/]*(\/[^?;]*)/)?[1] ? "/"
getPathComponents: ->
@getPath().split("/").slice(1)
getLastPathComponent: ->
@getPathComponents().slice(-1)[0]
getExtension: ->
@getLastPathComponent().match(/\.[^.]*$/)?[0] ? ""
isHTML: ->
@getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)
isPrefixedBy: (location) ->
prefixURL = getPrefixURL(location)
@isEqualTo(location) or stringStartsWith(@absoluteURL, prefixURL)
isEqualTo: (location) ->
@absoluteURL is location?.absoluteURL
toCacheKey: ->
@requestURL
toJSON: ->
@absoluteURL
toString: ->
@absoluteURL
valueOf: ->
@absoluteURL
# Private
getPrefixURL = (location) ->
addTrailingSlash(location.getOrigin() + location.getPath())
addTrailingSlash = (url) ->
if stringEndsWith(url, "/") then url else url + "/"
stringStartsWith = (string, prefix) ->
string.slice(0, prefix.length) is prefix
stringEndsWith = (string, suffix) ->
string.slice(-suffix.length) is suffix
@@ -0,0 +1,83 @@
class Turbolinks.ProgressBar
ANIMATION_DURATION = 300
@defaultCSS: """
.turbolinks-progress-bar {
position: fixed;
display: block;
top: 0;
left: 0;
height: 3px;
background: #0076ff;
z-index: 9999;
transition: width #{ANIMATION_DURATION}ms ease-out, opacity #{ANIMATION_DURATION / 2}ms #{ANIMATION_DURATION / 2}ms ease-in;
transform: translate3d(0, 0, 0);
}
"""
constructor: ->
@stylesheetElement = @createStylesheetElement()
@progressElement = @createProgressElement()
show: ->
unless @visible
@visible = true
@installStylesheetElement()
@installProgressElement()
@startTrickling()
hide: ->
if @visible and not @hiding
@hiding = true
@fadeProgressElement =>
@uninstallProgressElement()
@stopTrickling()
@visible = false
@hiding = false
setValue: (@value) ->
@refresh()
# Private
installStylesheetElement: ->
document.head.insertBefore(@stylesheetElement, document.head.firstChild)
installProgressElement: ->
@progressElement.style.width = 0
@progressElement.style.opacity = 1
document.documentElement.insertBefore(@progressElement, document.body)
@refresh()
fadeProgressElement: (callback) ->
@progressElement.style.opacity = 0
setTimeout(callback, ANIMATION_DURATION * 1.5)
uninstallProgressElement: ->
if @progressElement.parentNode
document.documentElement.removeChild(@progressElement)
startTrickling: ->
@trickleInterval ?= setInterval(@trickle, ANIMATION_DURATION)
stopTrickling: ->
clearInterval(@trickleInterval)
@trickleInterval = null
trickle: =>
@setValue(@value + Math.random() / 100)
refresh: ->
requestAnimationFrame =>
@progressElement.style.width = "#{10 + (@value * 90)}%"
createStylesheetElement: ->
element = document.createElement("style")
element.type = "text/css"
element.textContent = @constructor.defaultCSS
element
createProgressElement: ->
element = document.createElement("div")
element.className = "turbolinks-progress-bar"
element
@@ -0,0 +1,28 @@
class Turbolinks.Renderer
@render: (delegate, callback, args...) ->
renderer = new this args...
renderer.delegate = delegate
renderer.render(callback)
renderer
renderView: (callback) ->
@delegate.viewWillRender(@newBody)
callback()
@delegate.viewRendered(@newBody)
invalidateView: ->
@delegate.viewInvalidated()
createScriptElement: (element) ->
if element.getAttribute("data-turbolinks-eval") is "false"
element
else
createdScriptElement = document.createElement("script")
createdScriptElement.textContent = element.textContent
createdScriptElement.async = false
copyElementAttributes(createdScriptElement, element)
createdScriptElement
copyElementAttributes = (destinationElement, sourceElement) ->
for {name, value} in sourceElement.attributes
destinationElement.setAttribute(name, value)
@@ -0,0 +1,16 @@
do ->
return unless element = script = document.currentScript
return if script.hasAttribute("data-turbolinks-suppress-warning")
while element = element.parentNode
if element is document.body
return console.warn """
You are loading Turbolinks from a <script> element inside the <body> element. This is probably not what you meant to do!
Load your applications JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
For more information, see: https://github.com/turbolinks/turbolinks#working-with-script-elements
——
Suppress this warning by adding a `data-turbolinks-suppress-warning` attribute to: %s
""", script.outerHTML
@@ -0,0 +1,28 @@
class Turbolinks.ScrollManager
constructor: (@delegate) ->
@onScroll = Turbolinks.throttle(@onScroll)
start: ->
unless @started
addEventListener("scroll", @onScroll, false)
@onScroll()
@started = true
stop: ->
if @started
removeEventListener("scroll", @onScroll, false)
@started = false
scrollToElement: (element) ->
element.scrollIntoView()
scrollToPosition: ({x, y}) ->
window.scrollTo(x, y)
onScroll: (event) =>
@updatePosition(x: window.pageXOffset, y: window.pageYOffset)
# Private
updatePosition: (@position) ->
@delegate?.scrollPositionChanged(@position)
@@ -0,0 +1,65 @@
#= require ./head_details
class Turbolinks.Snapshot
@wrap: (value) ->
if value instanceof this
value
else if typeof value == "string"
@fromHTMLString(value)
else
@fromHTMLElement(value)
@fromHTMLString: (html) ->
htmlElement = document.createElement("html")
htmlElement.innerHTML = html
@fromHTMLElement(htmlElement)
@fromHTMLElement: (htmlElement) ->
headElement = htmlElement.querySelector("head")
bodyElement = htmlElement.querySelector("body") ? document.createElement("body")
headDetails = Turbolinks.HeadDetails.fromHeadElement(headElement)
new this headDetails, bodyElement
constructor: (@headDetails, @bodyElement) ->
clone: ->
new @constructor @headDetails, @bodyElement.cloneNode(true)
getRootLocation: ->
root = @getSetting("root") ? "/"
new Turbolinks.Location root
getCacheControlValue: ->
@getSetting("cache-control")
getElementForAnchor: (anchor) ->
try @bodyElement.querySelector("[id='#{anchor}'], a[name='#{anchor}']")
getPermanentElements: ->
@bodyElement.querySelectorAll("[id][data-turbolinks-permanent]")
getPermanentElementById: (id) ->
@bodyElement.querySelector("##{id}[data-turbolinks-permanent]")
getPermanentElementsPresentInSnapshot: (snapshot) ->
element for element in @getPermanentElements() when snapshot.getPermanentElementById(element.id)
findFirstAutofocusableElement: ->
@bodyElement.querySelector("[autofocus]")
hasAnchor: (anchor) ->
@getElementForAnchor(anchor)?
isPreviewable: ->
@getCacheControlValue() isnt "no-preview"
isCacheable: ->
@getCacheControlValue() isnt "no-cache"
isVisitable: ->
@getSetting("visit-control") isnt "reload"
# Private
getSetting: (name) ->
@headDetails.getMetaValue("turbolinks-#{name}")
@@ -0,0 +1,43 @@
class Turbolinks.SnapshotCache
constructor: (@size) ->
@keys = []
@snapshots = {}
has: (location) ->
key = keyForLocation(location)
key of @snapshots
get: (location) ->
return unless @has(location)
snapshot = @read(location)
@touch(location)
snapshot
put: (location, snapshot) ->
@write(location, snapshot)
@touch(location)
snapshot
# Private
read: (location) ->
key = keyForLocation(location)
@snapshots[key]
write: (location, snapshot) ->
key = keyForLocation(location)
@snapshots[key] = snapshot
touch: (location) ->
key = keyForLocation(location)
index = @keys.indexOf(key)
@keys.splice(index, 1) if index > -1
@keys.unshift(key)
@trim()
trim: ->
for key in @keys.splice(@size)
delete @snapshots[key]
keyForLocation = (location) ->
Turbolinks.Location.wrap(location).toCacheKey()
@@ -0,0 +1,104 @@
#= require ./renderer
class Turbolinks.SnapshotRenderer extends Turbolinks.Renderer
constructor: (@currentSnapshot, @newSnapshot, @isPreview) ->
@currentHeadDetails = @currentSnapshot.headDetails
@newHeadDetails = @newSnapshot.headDetails
@currentBody = @currentSnapshot.bodyElement
@newBody = @newSnapshot.bodyElement
render: (callback) ->
if @shouldRender()
@mergeHead()
@renderView =>
@replaceBody()
@focusFirstAutofocusableElement() unless @isPreview
callback()
else
@invalidateView()
mergeHead: ->
@copyNewHeadStylesheetElements()
@copyNewHeadScriptElements()
@removeCurrentHeadProvisionalElements()
@copyNewHeadProvisionalElements()
replaceBody: ->
placeholders = @relocateCurrentBodyPermanentElements()
@activateNewBodyScriptElements()
@assignNewBody()
@replacePlaceholderElementsWithClonedPermanentElements(placeholders)
shouldRender: ->
@newSnapshot.isVisitable() and @trackedElementsAreIdentical()
trackedElementsAreIdentical: ->
@currentHeadDetails.getTrackedElementSignature() is @newHeadDetails.getTrackedElementSignature()
copyNewHeadStylesheetElements: ->
for element in @getNewHeadStylesheetElements()
document.head.appendChild(element)
copyNewHeadScriptElements: ->
for element in @getNewHeadScriptElements()
document.head.appendChild(@createScriptElement(element))
removeCurrentHeadProvisionalElements: ->
for element in @getCurrentHeadProvisionalElements()
document.head.removeChild(element)
copyNewHeadProvisionalElements: ->
for element in @getNewHeadProvisionalElements()
document.head.appendChild(element)
relocateCurrentBodyPermanentElements: ->
for permanentElement in @getCurrentBodyPermanentElements()
placeholder = createPlaceholderForPermanentElement(permanentElement)
newElement = @newSnapshot.getPermanentElementById(permanentElement.id)
replaceElementWithElement(permanentElement, placeholder.element)
replaceElementWithElement(newElement, permanentElement)
placeholder
replacePlaceholderElementsWithClonedPermanentElements: (placeholders) ->
for { element, permanentElement } in placeholders
clonedElement = permanentElement.cloneNode(true)
replaceElementWithElement(element, clonedElement)
activateNewBodyScriptElements: ->
for inertScriptElement in @getNewBodyScriptElements()
activatedScriptElement = @createScriptElement(inertScriptElement)
replaceElementWithElement(inertScriptElement, activatedScriptElement)
assignNewBody: ->
document.body = @newBody
focusFirstAutofocusableElement: ->
@newSnapshot.findFirstAutofocusableElement()?.focus()
getNewHeadStylesheetElements: ->
@newHeadDetails.getStylesheetElementsNotInDetails(@currentHeadDetails)
getNewHeadScriptElements: ->
@newHeadDetails.getScriptElementsNotInDetails(@currentHeadDetails)
getCurrentHeadProvisionalElements: ->
@currentHeadDetails.getProvisionalElements()
getNewHeadProvisionalElements: ->
@newHeadDetails.getProvisionalElements()
getCurrentBodyPermanentElements: ->
@currentSnapshot.getPermanentElementsPresentInSnapshot(@newSnapshot)
getNewBodyScriptElements: ->
@newBody.querySelectorAll("script")
createPlaceholderForPermanentElement = (permanentElement) ->
element = document.createElement("meta")
element.setAttribute("name", "turbolinks-permanent-placeholder")
element.setAttribute("content", permanentElement.id)
{ element, permanentElement }
replaceElementWithElement = (fromElement, toElement) ->
if parentElement = fromElement.parentNode
parentElement.replaceChild(toElement, fromElement)
@@ -0,0 +1,18 @@
Turbolinks.start = ->
if installTurbolinks()
Turbolinks.controller ?= createController()
Turbolinks.controller.start()
installTurbolinks = ->
window.Turbolinks ?= Turbolinks
moduleIsInstalled()
createController = ->
controller = new Turbolinks.Controller
controller.adapter = new Turbolinks.BrowserAdapter(controller)
controller
moduleIsInstalled = ->
window.Turbolinks is Turbolinks
Turbolinks.start() if moduleIsInstalled()
@@ -0,0 +1,37 @@
#= require ./snapshot
#= require ./snapshot_renderer
#= require ./error_renderer
class Turbolinks.View
constructor: (@delegate) ->
@htmlElement = document.documentElement
getRootLocation: ->
@getSnapshot().getRootLocation()
getElementForAnchor: (anchor) ->
@getSnapshot().getElementForAnchor(anchor)
getSnapshot: ->
Turbolinks.Snapshot.fromHTMLElement(@htmlElement)
render: ({snapshot, error, isPreview}, callback) ->
@markAsPreview(isPreview)
if snapshot?
@renderSnapshot(snapshot, isPreview, callback)
else
@renderError(error, callback)
# Private
markAsPreview: (isPreview) ->
if isPreview
@htmlElement.setAttribute("data-turbolinks-preview", "")
else
@htmlElement.removeAttribute("data-turbolinks-preview")
renderSnapshot: (snapshot, isPreview, callback) ->
Turbolinks.SnapshotRenderer.render(@delegate, callback, @getSnapshot(), Turbolinks.Snapshot.wrap(snapshot), isPreview)
renderError: (error, callback) ->
Turbolinks.ErrorRenderer.render(@delegate, callback, error)
@@ -0,0 +1,162 @@
#= require ./http_request
class Turbolinks.Visit
constructor: (@controller, location, @action) ->
@identifier = Turbolinks.uuid()
@location = Turbolinks.Location.wrap(location)
@adapter = @controller.adapter
@state = "initialized"
@timingMetrics = {}
start: ->
if @state is "initialized"
@recordTimingMetric("visitStart")
@state = "started"
@adapter.visitStarted(this)
cancel: ->
if @state is "started"
@request?.cancel()
@cancelRender()
@state = "canceled"
complete: ->
if @state is "started"
@recordTimingMetric("visitEnd")
@state = "completed"
@adapter.visitCompleted?(this)
@controller.visitCompleted(this)
fail: ->
if @state is "started"
@state = "failed"
@adapter.visitFailed?(this)
changeHistory: ->
unless @historyChanged
actionForHistory = if @location.isEqualTo(@referrer) then "replace" else @action
method = getHistoryMethodForAction(actionForHistory)
@controller[method](@location, @restorationIdentifier)
@historyChanged = true
issueRequest: ->
if @shouldIssueRequest() and not @request?
@progress = 0
@request = new Turbolinks.HttpRequest this, @location, @referrer
@request.send()
getCachedSnapshot: ->
if snapshot = @controller.getCachedSnapshotForLocation(@location)
if not @location.anchor? or snapshot.hasAnchor(@location.anchor)
if @action is "restore" or snapshot.isPreviewable()
snapshot
hasCachedSnapshot: ->
@getCachedSnapshot()?
loadCachedSnapshot: ->
if snapshot = @getCachedSnapshot()
isPreview = @shouldIssueRequest()
@render ->
@cacheSnapshot()
@controller.render({snapshot, isPreview}, @performScroll)
@adapter.visitRendered?(this)
@complete() unless isPreview
loadResponse: ->
if @response?
@render ->
@cacheSnapshot()
if @request.failed
@controller.render(error: @response, @performScroll)
@adapter.visitRendered?(this)
@fail()
else
@controller.render(snapshot: @response, @performScroll)
@adapter.visitRendered?(this)
@complete()
followRedirect: ->
if @redirectedToLocation and not @followedRedirect
@location = @redirectedToLocation
@controller.replaceHistoryWithLocationAndRestorationIdentifier(@redirectedToLocation, @restorationIdentifier)
@followedRedirect = true
# HTTP Request delegate
requestStarted: ->
@recordTimingMetric("requestStart")
@adapter.visitRequestStarted?(this)
requestProgressed: (@progress) ->
@adapter.visitRequestProgressed?(this)
requestCompletedWithResponse: (@response, redirectedToLocation) ->
@redirectedToLocation = Turbolinks.Location.wrap(redirectedToLocation) if redirectedToLocation?
@adapter.visitRequestCompleted(this)
requestFailedWithStatusCode: (statusCode, @response) ->
@adapter.visitRequestFailedWithStatusCode(this, statusCode)
requestFinished: ->
@recordTimingMetric("requestEnd")
@adapter.visitRequestFinished?(this)
# Scrolling
performScroll: =>
unless @scrolled
if @action is "restore"
@scrollToRestoredPosition() or @scrollToTop()
else
@scrollToAnchor() or @scrollToTop()
@scrolled = true
scrollToRestoredPosition: ->
position = @restorationData?.scrollPosition
if position?
@controller.scrollToPosition(position)
true
scrollToAnchor: ->
if @location.anchor?
@controller.scrollToAnchor(@location.anchor)
true
scrollToTop: ->
@controller.scrollToPosition(x: 0, y: 0)
# Instrumentation
recordTimingMetric: (name) ->
@timingMetrics[name] ?= new Date().getTime()
getTimingMetrics: ->
Turbolinks.copyObject(@timingMetrics)
# Private
getHistoryMethodForAction = (action) ->
switch action
when "replace" then "replaceHistoryWithLocationAndRestorationIdentifier"
when "advance", "restore" then "pushHistoryWithLocationAndRestorationIdentifier"
shouldIssueRequest: ->
if @action is "restore"
not @hasCachedSnapshot()
else
true
cacheSnapshot: ->
unless @snapshotCached
@controller.cacheSnapshot()
@snapshotCached = true
render: (callback) ->
@cancelRender()
@frame = requestAnimationFrame =>
@frame = null
callback.call(this)
cancelRender: ->
cancelAnimationFrame(@frame) if @frame