public/icons/*your_scraper_name*/ are up-to-date. If you pull the updated icon from a place different than the one specified in the `SOURCE` file, make sure to replace the old link with the new one.
+4. If `self.links` is defined, check if the urls are still correct.
+5. If the scraper inherits from `FileScraper` rather than `URLScraper`, follow the instructions for that scraper in [`file-scrapers.md`](../docs/file-scrapers.md) to obtain the source material for the scraper.
+6. Generate the docs using `thor docs:generate public/icons/*your_scraper_name*/ directory:
+ - [ ] `16.png`: a 16×16 pixel icon for the doc
+ - [ ] `16@2x.png`: a 32×32 pixel icon for the doc
+ - [ ] `SOURCE`: A text file containing the URL to the page the image can be found on or the URL of the original image itself
+
+
+
+
+If you're updating existing documentation to its latest version, please ensure that you have:
+
+- [ ] Updated the versions and releases in the scraper file
+- [ ] Ensured the license is up-to-date
+- [ ] Ensured the icons and the `SOURCE` file in public/icons/*your_scraper_name*/ are up-to-date if the documentation has a custom icon
+- [ ] Ensured `self.links` contains up-to-date urls if `self.links` is defined
+- [ ] Tested the changes locally to ensure:
+ - The scraper still works without errors
+ - The scraped documentation still looks consistent with the rest of DevDocs
+ - The categorization of entries is still good
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000000..faac4b06cc
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,31 @@
+name: Deploy
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ deploy:
+ name: Deploy to Heroku
+ runs-on: ubuntu-24.04
+ if: github.repository == 'freeCodeCamp/devdocs'
+ steps:
+ - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
+ with:
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
+ - name: Run tests
+ run: bundle exec rake
+ - name: Install Heroku CLI
+ run: |
+ curl https://cli-assets.heroku.com/install.sh | sh
+ - name: Deploy to Heroku
+ uses: akhileshns/heroku-deploy@e3eb99d45a8e2ec5dca08735e089607befa4bf28 # v3.14.15
+ with:
+ heroku_api_key: ${{secrets.HEROKU_API_KEY}}
+ heroku_app_name: "devdocs"
+ heroku_email: "team@freecodecamp.com"
+ dontuseforce: true # --force should never be necessary
+ dontautocreate: true # The app exists, it should not be created
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 0000000000..dd0ac8a062
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,54 @@
+name: Build and Push Docker Images
+on:
+ schedule:
+ - cron: '0 0 1 * *' # Run monthly on the 1st
+ workflow_dispatch: # Allow manual triggers
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ strategy:
+ matrix:
+ variant:
+ - name: regular
+ file: Dockerfile
+ suffix: ''
+ - name: alpine
+ file: Dockerfile-alpine
+ suffix: '-alpine'
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ with:
+ persist-credentials: false
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata for Docker
+ id: meta
+ uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=raw,value=latest${{ matrix.variant.suffix }}
+ type=raw,value={{date 'YYYYMMDD'}}${{ matrix.variant.suffix }}
+
+ - name: Build and push image
+ uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
+ with:
+ context: .
+ file: ./${{ matrix.variant.file }}
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/schedule-doc-report.yml b/.github/workflows/schedule-doc-report.yml
new file mode 100644
index 0000000000..b642089ec6
--- /dev/null
+++ b/.github/workflows/schedule-doc-report.yml
@@ -0,0 +1,18 @@
+name: Generate documentation versions report
+on:
+ schedule:
+ - cron: '17 4 1 * *'
+ workflow_dispatch:
+
+jobs:
+ report:
+ runs-on: ubuntu-24.04
+ if: github.repository == 'freeCodeCamp/devdocs'
+ steps:
+ - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
+ with:
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
+ - name: Generate report
+ run: bundle exec thor updates:check --github-token ${{ secrets.DEVDOCS_BOT_PAT }} --upload
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000000..555b6a0c78
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,18 @@
+name: Ruby tests
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
+ with:
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
+ - name: Run tests
+ run: bundle exec rake
diff --git a/.gitignore b/.gitignore
index 1060fcf0c7..6f55be6a9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,16 @@
.DS_Store
.bundle
+log
tmp
public/assets
public/fonts
public/docs/**/*
docs/**/*
!docs/*.md
+/vendor
+*.tar
+*.tar.bz2
+*.tar.gz
+*.zip
+assets/stylesheets/components/_environment.scss
+assets/stylesheets/global/_icons.scss
diff --git a/.ruby-version b/.ruby-version
index 914ec96711..7921bd0c89 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.6.0
\ No newline at end of file
+3.4.8
diff --git a/.slugignore b/.slugignore
index 6a7070f5ec..9daeafb986 100644
--- a/.slugignore
+++ b/.slugignore
@@ -1,2 +1 @@
-public/icons
-test
\ No newline at end of file
+test
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000000..58766197c0
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1 @@
+ruby 3.4.8
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d5e5a6aa34..0000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: ruby
-
-cache: bundler
-
-before_script:
- - gem update --system
- - gem install bundler
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 26f8ab09e8..0000000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,2 +0,0 @@
-
-> Our Code of Conduct is available here: public/icons/*your_scraper_name*/ directory:
- - [ ] `16.png`: a 16×16 pixel icon for the doc
- - [ ] `16@2x.png`: a 32×32 pixel icon for the doc
- - [ ] `SOURCE`: A text file containing the URL to the page the image can be found on or the URL of the original image itself
-
-
diff --git a/README.md b/README.md
index 3c652e9fd2..cf6c735a76 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,56 @@
-# [DevDocs](https://devdocs.io) — API Documentation Browser [](https://travis-ci.org/freeCodeCamp/devdocs)
+# [DevDocs](https://devdocs.io) — API Documentation Browser
DevDocs combines multiple developer documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more.
DevDocs was created by [Thibaut Courouble](https://thibaut.me) and is operated by [freeCodeCamp](https://www.freecodecamp.org).
+## We are currently searching for maintainers
+
+Please reach out to the community on [Discord](https://discord.gg/PRyKn3Vbay) if you would like to join the team!
+
Keep track of development news:
-* Join the contributor chat room on [Gitter](https://gitter.im/FreeCodeCamp/DevDocs)
+* Join the `#contributors` chat room on [Discord](https://discord.gg/PRyKn3Vbay)
* Watch the repository on [GitHub](https://github.com/freeCodeCamp/devdocs/subscription)
* Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter
-**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [Documentation](#documentation) · [Plugins and Extensions](#plugins-and-extensions) · [License](#copyright--license) · [Questions?](#questions)
+**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [Documentation](#documentation) · [Related Projects](#related-projects) · [License](#copyright--license) · [Questions?](#questions)
## Quick Start
Unless you wish to contribute to the project, we recommend using the hosted version at [devdocs.io](https://devdocs.io). It's up-to-date and works offline out-of-the-box.
-DevDocs is made of two pieces: a Ruby scraper that generates the documentation and metadata, and a JavaScript app powered by a small Sinatra app.
+### Using Docker (Recommended)
-DevDocs requires Ruby 2.6.0, libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). Once you have these installed, run the following commands:
+The easiest way to run DevDocs locally is using Docker:
+```sh
+docker run --name devdocs -d -p 9292:9292 ghcr.io/freecodecamp/devdocs:latest
```
+
+This will start DevDocs at [localhost:9292](http://localhost:9292). We provide both regular and Alpine-based images:
+- `ghcr.io/freecodecamp/devdocs:latest` - Standard image
+- `ghcr.io/freecodecamp/devdocs:latest-alpine` - Alpine-based (smaller size)
+
+Images are automatically built and updated monthly with the latest documentation.
+
+Alternatively, you can build the image yourself:
+
+```sh
+git clone https://github.com/freeCodeCamp/devdocs.git && cd devdocs
+docker build -t devdocs .
+docker run --name devdocs -d -p 9292:9292 devdocs
+```
+
+### Manual Installation
+
+DevDocs is made of two pieces: a Ruby scraper that generates the documentation and metadata, and a JavaScript app powered by a small Sinatra app.
+
+DevDocs requires Ruby 3.4.1 (defined in [`Gemfile`](./Gemfile)), libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). On Arch Linux run `pacman -S ruby ruby-bundler ruby-erb ruby-irb`.
+
+Once you have these installed, run the following commands:
+
+```sh
git clone https://github.com/freeCodeCamp/devdocs.git && cd devdocs
gem install bundler
bundle install
@@ -30,43 +60,41 @@ bundle exec rackup
Finally, point your browser at [localhost:9292](http://localhost:9292) (the first request will take a few seconds to compile the assets). You're all set.
-The `thor docs:download` command is used to download pre-generated documentations from DevDocs's servers (e.g. `thor docs:download html css`). You can see the list of available documentations and versions by running `thor docs:list`. To update all downloaded documentations, run `thor docs:download --installed`.
+The `thor docs:download` command is used to download pre-generated documentations from DevDocs's servers (e.g. `thor docs:download html css`). You can see the list of available documentations and versions by running `thor docs:list`. To update all downloaded documentations, run `thor docs:download --installed`. To download and install all documentation this project has available, run `thor docs:download --all`.
-**Note:** there is currently no update mechanism other than `git pull origin master` to update the code and `thor docs:download --installed` to download the latest version of the docs. To stay informed about new releases, be sure to [watch](https://github.com/freeCodeCamp/devdocs/subscription) this repository.
-
-Alternatively, DevDocs may be started as a Docker container:
-
-```
-# First, build the image
-git clone https://github.com/freeCodeCamp/devdocs.git && cd devdocs
-docker build -t thibaut/devdocs .
-
-# Finally, start a DevDocs container (access http://localhost:9292)
-docker run --name devdocs -d -p 9292:9292 thibaut/devdocs
-```
+**Note:** there is currently no update mechanism other than `git pull origin main` to update the code and `thor docs:download --installed` to download the latest version of the docs. To stay informed about new releases, be sure to [watch](https://github.com/freeCodeCamp/devdocs/subscription) this repository.
## Vision
DevDocs aims to make reading and searching reference documentation fast, easy and enjoyable.
-The app's main goals are to: keep load times as short as possible; improve the quality, speed, and order of search results; maximize the use of caching and other performance optimizations; maintain a clean and readable user interface; be fully functional offline; support full keyboard navigation; reduce “context switch” by using a consistent typography and design across all documentations; reduce clutter by focusing on a specific category of content (API/reference) and indexing only the minimum useful to most developers.
+The app's main goals are to:
+
+* Keep load times as short as possible
+* Improve the quality, speed, and order of search results
+* Maximize the use of caching and other performance optimizations
+* Maintain a clean and readable user interface
+* Be fully functional offline
+* Support full keyboard navigation
+* Reduce “context switch” by using a consistent typography and design across all documentations
+* Reduce clutter by focusing on a specific category of content (API/reference) and indexing only the minimum useful to most developers.
**Note:** DevDocs is neither a programming guide nor a search engine. All our content is pulled from third-party sources and the project doesn't intend to compete with full-text search engines. Its backbone is metadata; each piece of content is identified by a unique, "obvious" and short string. Tutorials, guides and other content that don't meet this requirement are outside the scope of the project.
## App
-The web app is all client-side JavaScript, written in [CoffeeScript](http://coffeescript.org), and powered by a small [Sinatra](http://www.sinatrarb.com)/[Sprockets](https://github.com/rails/sprockets) application. It relies on files generated by the [scraper](#scraper).
+The web app is all client-side JavaScript, powered by a small [Sinatra](http://www.sinatrarb.com)/[Sprockets](https://github.com/rails/sprockets) application. It relies on files generated by the [scraper](#scraper).
Many of the code's design decisions were driven by the fact that the app uses XHR to load content directly into the main frame. This includes stripping the original documents of most of their HTML markup (e.g. scripts and stylesheets) to avoid polluting the main frame, and prefixing all CSS class names with an underscore to prevent conflicts.
-Another driving factor is performance and the fact that everything happens in the browser. `applicationCache` (which comes with its own set of constraints) and `localStorage` are used to speed up the boot time, while memory consumption is kept in check by allowing the user to pick his/her own set of documentations. The search algorithm is kept simple because it needs to be fast even searching through 100,000 strings.
+Another driving factor is performance and the fact that everything happens in the browser. A service worker (which comes with its own set of constraints) and `localStorage` are used to speed up the boot time, while memory consumption is kept in check by allowing the user to pick his/her own set of documentations. The search algorithm is kept simple because it needs to be fast even searching through 100,000 strings.
DevDocs being a developer tool, the browser requirements are high:
* Recent versions of Firefox, Chrome, or Opera
-* Safari 9.1+
-* Edge 16+
-* iOS 10+
+* Safari 11.1+
+* Edge 17+
+* iOS 11.3+
This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!
@@ -95,7 +123,7 @@ More information about [scrapers](./docs/scraper-reference.md) and [filters](./d
The command-line interface uses [Thor](http://whatisthor.com). To see all commands and options, run `thor list` from the project's root.
-```
+```sh
# Server
rackup # Start the server (ctrl+c to stop)
rackup --help # List server options
@@ -112,10 +140,9 @@ thor docs:clean # Delete documentation packages
# Console
thor console # Start a REPL
thor console:docs # Start a REPL in the "Docs" module
-Note: tests can be run quickly from within the console using the "test" command. Run "help test"
-for usage instructions.
-# Tests
+# Tests can be run quickly from within the console using the "test" command.
+# Run "help test" for usage instructions.
thor test:all # Run all tests
thor test:docs # Run "Docs" tests
thor test:app # Run "App" tests
@@ -138,29 +165,57 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C
* [Filter Reference](./docs/filter-reference.md)
* [Maintainers’ Guide](./docs/maintainers.md)
-## Plugins and Extensions
-
-* [Chrome web app](https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe)
-* [Ubuntu Touch app](https://uappexplorer.com/app/devdocsunofficial.berkes)
-* [Sublime Text plugin](https://sublime.wbond.net/packages/DevDocs)
-* [Atom plugin](https://atom.io/packages/devdocs)
-* [Brackets extension](https://github.com/gruehle/dev-docs-viewer)
-* [Fluid](http://fluidapp.com) for turning DevDocs into a real OS X app
-* [GTK shell / Vim integration](https://github.com/naquad/devdocs-shell)
-* [Emacs lookup](https://github.com/skeeto/devdocs-lookup)
-* [Alfred Workflow](https://github.com/yannickglt/alfred-devdocs)
-* [Vim search plugin with Devdocs in its defaults](https://github.com/waiting-for-dev/vim-www) Just set `let g:www_shortcut_engines = { 'devdocs': ['Devdocs', 'alt + c shortcut to copy URL of original page."
+ ],
+ [
+ "2021-02-26",
+ "New documentation: React Bootstrap"
+ ],
+ [
+ "2021-01-03",
+ "New documentation: OCaml"
+ ],
+ [
+ "2020-12-23",
+ "New documentation: GTK"
+ ],
+ [
+ "2020-12-07",
+ "New documentations: Flask, Groovy, Jinja, Werkzeug"
+ ],
+ [
+ "2020-12-04",
+ "New documentation: HAProxy"
+ ],
+ [
+ "2020-11-17",
+ "TensorFlow has been split into TensorFlow Python, TensorFlow C++"
+ ],
+ [
+ "2020-11-14",
+ "New documentations: PyTorch, Spring Boot"
+ ],
+ [
+ "2020-01-13",
+ "New “Automatic” theme: match your browser or system dark mode setting. Enable it in preferences."
+ ],
+ [
+ "2020-01-13",
+ "New documentation: Gnuplot"
+ ],
+ [
+ "2019-10-26",
+ "New documentation: Sequelize"
+ ], [
+ "2019-10-20",
+ "New documentations: MariaDB and ReactiveX"
+ ], [
+ "2019-09-02",
+ "New documentations added over the last 3 weeks: Scala, WordPress, Cypress, SaltStack, Composer, Vue Router, Vuex, Pony, RxJS, Octave, Trio, Django REST Framework, Enzyme and GnuCOBOL"
+ ], [
+ "2019-07-21",
+ "Fixed several bugs, added an option to automatically download documentation and more."
+ ], [
+ "2019-07-19",
+ "Replaced the AppCache with a Service Worker (which makes DevDocs an installable PWA) and fixed layout preferences on Firefox."
+ ], [
"2018-09-23",
"New documentations: Puppeteer and Handlebars.js"
], [
diff --git a/assets/javascripts/templates/base.coffee b/assets/javascripts/templates/base.coffee
deleted file mode 100644
index 841d1e0bb7..0000000000
--- a/assets/javascripts/templates/base.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-app.templates.render = (name, value, args...) ->
- template = app.templates[name]
-
- if Array.isArray(value)
- result = ''
- result += template(val, args...) for val in value
- result
- else if typeof template is 'function'
- template(value, args...)
- else
- template
diff --git a/assets/javascripts/templates/base.js b/assets/javascripts/templates/base.js
new file mode 100644
index 0000000000..fc445ef19c
--- /dev/null
+++ b/assets/javascripts/templates/base.js
@@ -0,0 +1,15 @@
+app.templates.render = function (name, value, ...args) {
+ const template = app.templates[name];
+
+ if (Array.isArray(value)) {
+ let result = "";
+ for (var val of value) {
+ result += template(val, ...args);
+ }
+ return result;
+ } else if (typeof template === "function") {
+ return template(value, ...args);
+ } else {
+ return template;
+ }
+};
diff --git a/assets/javascripts/templates/error_tmpl.coffee b/assets/javascripts/templates/error_tmpl.coffee
deleted file mode 100644
index c4bf40ecbd..0000000000
--- a/assets/javascripts/templates/error_tmpl.coffee
+++ /dev/null
@@ -1,73 +0,0 @@
-error = (title, text = '', links = '') ->
- text = """#{text}
""" if text - links = """#{links}
""" if links - """#{exception.name}: #{exception.message} """
- when 'cant_open'
- """ An error occurred when trying to open the IndexedDB database:#{exception.name}: #{exception.message}DevDocs is an API documentation browser which supports the following browsers: -
- If you're unable to upgrade, we apologize. - We decided to prioritize speed and new features over support for older browsers. -
- Note: if you're already using one of the browsers above, check your settings and add-ons. - The app uses feature detection, not user agent sniffing. -
- — @DevDocs -
${text}
`; + } + if (links) { + links = `${links}
`; + } + return `${exception.name}: ${exception.message} `;
+ case "cant_open":
+ return ` An error occurred when trying to open the IndexedDB database:${exception.name}: ${exception.message}DevDocs is an API documentation browser which supports the following browsers: +
+ If you're unable to upgrade, we apologize. + We decided to prioritize speed and new features over support for older browsers. +
+ Note: if you're already using one of the browsers above, check your settings and add-ons. + The app uses feature detection, not user agent sniffing. +
+ — @DevDocs +
#{text}
""" - -app.templates.singleDocNotice = (doc) -> - notice """ You're browsing the #{doc.fullName} documentation. To browse all docs, go to - #{app.config.production_host} (or pressesc). """
-
-app.templates.disabledDocNotice = ->
- notice """ This documentation is disabled.
- To enable it, go to Preferences. """
diff --git a/assets/javascripts/templates/notice_tmpl.js b/assets/javascripts/templates/notice_tmpl.js
new file mode 100644
index 0000000000..49793eb571
--- /dev/null
+++ b/assets/javascripts/templates/notice_tmpl.js
@@ -0,0 +1,9 @@
+const notice = (text) => `${text}
`; + +app.templates.singleDocNotice = (doc) => + notice(` You're browsing the ${doc.fullName} documentation. To browse all docs, go to +${app.config.production_host} (or pressesc). `);
+
+app.templates.disabledDocNotice = () =>
+ notice(` This documentation is disabled.
+To enable it, go to Preferences. `);
diff --git a/assets/javascripts/templates/notif_tmpl.coffee b/assets/javascripts/templates/notif_tmpl.coffee
deleted file mode 100644
index 93611a5c12..0000000000
--- a/assets/javascripts/templates/notif_tmpl.coffee
+++ /dev/null
@@ -1,70 +0,0 @@
-notif = (title, html) ->
- html = html.replace /#{title}
- #{html}
-
- """
-
-textNotif = (title, message) ->
- notif title, """#{message}"""
-
-app.templates.notifUpdateReady = ->
- textNotif """DevDocs has been updated.""",
- """Reload the page to use the new version."""
-
-app.templates.notifError = ->
- textNotif """ Oops, an error occurred. """,
- """ Try reloading, and if the problem persists,
- resetting the app.
- You can also report this issue on GitHub. """
-
-app.templates.notifQuotaExceeded = ->
- textNotif """ The offline database has exceeded its size limitation. """,
- """ Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. """
-
-app.templates.notifCookieBlocked = ->
- textNotif """ Please enable cookies. """,
- """ DevDocs will not work properly if cookies are disabled. """
-
-app.templates.notifInvalidLocation = ->
- textNotif """ DevDocs must be loaded from #{app.config.production_host} """,
- """ Otherwise things are likely to break. """
-
-app.templates.notifImportInvalid = ->
- textNotif """ Oops, an error occurred. """,
- """ The file you selected is invalid. """
-
-app.templates.notifNews = (news) ->
- notif 'Changelog', """
→ #{doc.release}" if doc.release
- html += 'Disabled:' - html += '
→ #{doc.release}" if doc.release
- html += """Enable"""
- html += '${message}`);
+
+app.templates.notifUpdateReady = () =>
+ textNotif(
+ 'DevDocs has been updated.',
+ 'Reload the page to use the new version.',
+ );
+
+app.templates.notifError = () =>
+ textNotif(
+ " Oops, an error occurred. ",
+ ` Try reloading, and if the problem persists,
+resetting the app.
+You can also report this issue on GitHub. `,
+ );
+
+app.templates.notifQuotaExceeded = () =>
+ textNotif(
+ " The offline database has exceeded its size limitation. ",
+ " Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. ",
+ );
+
+app.templates.notifCookieBlocked = () =>
+ textNotif(
+ " Please enable cookies. ",
+ " DevDocs will not work properly if cookies are disabled. ",
+ );
+
+app.templates.notifInvalidLocation = () =>
+ textNotif(
+ ` DevDocs must be loaded from ${app.config.production_host} `,
+ " Otherwise things are likely to break. ",
+ );
+
+app.templates.notifImportInvalid = () =>
+ textNotif(
+ " Oops, an error occurred. ",
+ " The file you selected is invalid. ",
+ );
+
+app.templates.notifNews = (news) =>
+ notif(
+ "Changelog",
+ `
→ ${doc.release}`;
+ }
+ }
+ html += "Disabled:'; + html += '
→ ${doc.release}`;
+ }
+ html += 'Enable';
+ }
+ html += "DevDocs combines multiple developer documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more. -
DevDocs is free and open source. It was created by Thibaut Courouble and is operated by freeCodeCamp. -
To keep up-to-date with the latest news: -
- -
- Copyright 2013–2019 Thibaut Courouble and other contributors
- This software is licensed under the terms of the Mozilla Public License v2.0.
- You may obtain a copy of the source code at github.com/freeCodeCamp/devdocs.
- For more information, see the COPYRIGHT
- and LICENSE files.
-
-
Special thanks to: -
| Documentation - | Copyright - | License - #{(" |
|---|---|---|
| #{c[0]} | © #{c[1]} | #{c[2]}" for c in credits).join('')} - |
DevDocs combines multiple developer documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more. +
DevDocs is free and open source. It was created by Thibaut Courouble and is operated by freeCodeCamp. +
To keep up-to-date with the latest news: +
+ +
+ Copyright 2013–2026 Thibaut Courouble and other contributors
+ This software is licensed under the terms of the Mozilla Public License v2.0.
+ You may obtain a copy of the source code at github.com/freeCodeCamp/devdocs.
+ For more information, see the COPYRIGHT
+ and LICENSE files.
+
+
Special thanks to: +
| Documentation + | Copyright/License + | Source code + ${docs + .map( + (doc) => + ` |
|---|---|---|
| ${doc.name} | ${doc.attribution} | Source code |
- Documentations can be enabled and disabled in the Preferences. - Alternatively, you can enable a documentation by searching for it in the main search - and clicking the "Enable" link in the results. - For faster and better search, only enable the documentations you plan on actively using. -
- Once a documentation is enabled, it becomes part of the search and its content can be downloaded for offline access — and faster page loads when online — in the Offline area. - -
- The search is case-insensitive and ignores whitespace. It supports fuzzy matching
- (e.g. bgcp matches background-clip)
- as well as aliases (full list below).
-
tab (space on mobile).
- For example, to search the JavaScript documentation, enter javascript
- or js, then tab.backspace or
- esc.
- #q= will be used as search query.tab when devdocs.io is autocompleted
- in the omnibox (to set a custom keyword, click Manage search engines\u2026 in Chrome's settings).
- - Note: the above search features only work for documentations that are enabled. - -
↓
- ↑
- →
- ←
- enter
- #{ctrlKey} + enter
- alt + r
- #{navKey} + ←
- #{navKey} + →
- alt + ↓
- alt + ↑
- shift + ↓
- shift + ↑
- space
- shift + space
- #{ctrlKey} + ↑
- #{ctrlKey} + ↓
- alt + f
- ctrl + ,
- esc
- ?
- alt + o
- alt + g
- alt + s
- alt + d
-
- Tip: If the cursor is no longer in the search field, press / or
- continue to type and it will refocus the search field and start showing new results.
-
-
| Word - | Alias - #{(" |
|---|---|
| #{key} | #{value}" for key, value of aliases_one).join('')} - |
| Word - | Alias - #{(" |
|---|---|
| #{key} | #{value}" for key, value of aliases_two).join('')} - |
Feel free to suggest new aliases on GitHub. -""" diff --git a/assets/javascripts/templates/pages/help_tmpl.js b/assets/javascripts/templates/pages/help_tmpl.js new file mode 100644 index 0000000000..e155d82999 --- /dev/null +++ b/assets/javascripts/templates/pages/help_tmpl.js @@ -0,0 +1,179 @@ +app.templates.helpPage = function () { + const ctrlKey = $.isMac() ? "cmd" : "ctrl"; + const navKey = $.isMac() ? "cmd" : "alt"; + const arrowScroll = app.settings.get("arrowScroll"); + + const aliases = Object.entries(app.config.docs_aliases); + const middle = Math.ceil(aliases.length / 2); + const aliases_one = aliases.slice(0, middle); + const aliases_two = aliases.slice(middle); + + return `\ + + +
+ Documentations can be enabled and disabled in the Preferences. + Alternatively, you can enable a documentation by searching for it in the main search + and clicking the "Enable" link in the results. + For faster and better search, only enable the documentations you plan on actively using. +
+ Once a documentation is enabled, it becomes part of the search and its content can be downloaded for offline access — and faster page loads when online — in the Offline area. + +
+ The search is case-insensitive and ignores whitespace. It supports fuzzy matching
+ (e.g. bgcp matches background-clip)
+ as well as aliases (full list below).
+
tab (space on mobile).
+ For example, to search the JavaScript documentation, enter javascript
+ or js, then tab.backspace or
+ esc.
+ #q= will be used as search query.tab when devdocs.io is autocompleted
+ in the omnibox (to set a custom keyword, click Manage search engines\u2026 in Chrome's settings).
+ + Note: the above search features only work for documentations that are enabled. + +
shift + ' : ""}
+ ↓
+ ↑
+ shift + ' : ""}
+ →
+ ←
+ enter
+ ${ctrlKey} + enter
+ alt + r
+ ${navKey} + ←
+ ${navKey} + →
+ ↓ ' +
+ '↑'
+ : 'alt + ↓ ' +
+ 'alt + ↑' +
+ "shift + ↓ ' +
+ 'shift + ↑'
+ }
+ space
+ shift + space
+ ${ctrlKey} + ↑
+ ${ctrlKey} + ↓
+ alt + f
+ ctrl + ,
+ esc
+ ?
+ alt + c
+ alt + o
+ alt + g
+ alt + s
+ alt + d
+
+ Tip: If the cursor is no longer in the search field, press / or
+ continue to type and it will refocus the search field and start showing new results.
+
+
| Word + | Alias + ${aliases_one + .map( + ([key, value]) => + ` |
|---|---|
| ${key} | ${value}`, + ) + .join("")} + |
| Word + | Alias + ${aliases_two + .map( + ([key, value]) => + ` |
|---|---|
| ${key} | ${value}`, + ) + .join("")} + |
Feel free to suggest new aliases on GitHub.\ +`; +}; diff --git a/assets/javascripts/templates/pages/news_tmpl.coffee.erb b/assets/javascripts/templates/pages/news_tmpl.coffee.erb deleted file mode 100644 index f6760a61e4..0000000000 --- a/assets/javascripts/templates/pages/news_tmpl.coffee.erb +++ /dev/null @@ -1,36 +0,0 @@ -#= depend_on news.json - -app.templates.newsPage = -> - """
- For the latest news, follow @DevDocs.
- For development updates, follow the project on GitHub.
-
+For the latest news, follow @DevDocs.
+For development updates, follow the project on GitHub.
+
| Documentation | -Size | -Status | -Action | -
|---|
Note: your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there. -
| Documentation | +Size | +Status | +Action | +
|---|
Note: your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there. +
ENABLE_SERVICE_WORKER environment variable to true)";
+ }
+
+ return ` No. Service Workers ${reason}, so loading devdocs.io offline won't work.DevDocs combines multiple API documentations in a fast, organized, and searchable interface. - Here's what you should know before you start: -
Happy coding! - Stop showing this message -
DevDocs is running inside an Android WebView. Some features may not work properly. -
If you downloaded an app called DevDocs on the Play Store, please uninstall it — it's made by someone who is using (and profiting from) the name DevDocs without permission. -
To install DevDocs on your phone, visit devdocs.io in Chrome and select "Add to home screen" in the menu. -
DevDocs combines multiple API documentations in a fast, organized, and searchable interface. + Here's what you should know before you start: +
Happy coding! + Stop showing this message +
DevDocs is running inside an Android WebView. Some features may not work properly. +
If you downloaded an app called DevDocs on the Play Store, please uninstall it — it's made by someone who is using (and profiting from) the name DevDocs without permission. +
To install DevDocs on your phone, visit devdocs.io in Chrome and select "Add to home screen" in the menu. +
- - - -
- -""" diff --git a/assets/javascripts/templates/pages/settings_tmpl.js b/assets/javascripts/templates/pages/settings_tmpl.js new file mode 100644 index 0000000000..cfd30de1b2 --- /dev/null +++ b/assets/javascripts/templates/pages/settings_tmpl.js @@ -0,0 +1,118 @@ +const themeOption = ({ label, value }, settings) => `\ +\ +`; + +app.templates.settingsPage = (settings) => `\ +
+ + + +
+ \ +`; diff --git a/assets/javascripts/templates/pages/type_tmpl.coffee b/assets/javascripts/templates/pages/type_tmpl.coffee deleted file mode 100644 index c419a6a8c4..0000000000 --- a/assets/javascripts/templates/pages/type_tmpl.coffee +++ /dev/null @@ -1,6 +0,0 @@ -app.templates.typePage = (type) -> - """
- ProTip - (click to dismiss) -
- Hit ↓ ↑ ← → to navigate the sidebar.
- Hit space / shift space, alt ↓/↑ or shift ↓/↑ to scroll the page.
-
- See all keyboard shortcuts -""" diff --git a/assets/javascripts/templates/tip_tmpl.js b/assets/javascripts/templates/tip_tmpl.js new file mode 100644 index 0000000000..223ffe9587 --- /dev/null +++ b/assets/javascripts/templates/tip_tmpl.js @@ -0,0 +1,16 @@ +app.templates.tipKeyNav = () => `\ +
+ ProTip + (click to dismiss) +
+ Hit ${
+ app.settings.get("arrowScroll") ? 'shift +' : ""
+ } ↓ ↑ ← → to navigate the sidebar.
+ Hit space / shift space${
+ app.settings.get("arrowScroll")
+ ? ' or ↓/↑'
+ : ', alt ↓/↑ or shift ↓/↑'
+ } to scroll the page.
+
+ See all keyboard shortcuts\ +`; diff --git a/assets/javascripts/tracking.js b/assets/javascripts/tracking.js index ca05b21820..c15781f5c3 100644 --- a/assets/javascripts/tracking.js +++ b/assets/javascripts/tracking.js @@ -1,28 +1,55 @@ try { - if (app.config.env == 'production') { - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-5544833-12', 'devdocs.io'); - page.track(function() { - ga('send', 'pageview', { - page: location.pathname + location.search + location.hash, - dimension1: app.router.context && app.router.context.doc && app.router.context.doc.slug_without_version + if (app.config.env === "production") { + if (Cookies.get("analyticsConsent") === "1") { + (function (i, s, o, g, r, a, m) { + i["GoogleAnalyticsObject"] = r; + (i[r] = + i[r] || + function () { + (i[r].q = i[r].q || []).push(arguments); + }), + (i[r].l = 1 * new Date()); + (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m); + })( + window, + document, + "script", + "https://www.google-analytics.com/analytics.js", + "ga", + ); + ga("create", "UA-5544833-12", "devdocs.io"); + page.track(function () { + ga("send", "pageview", { + page: location.pathname + location.search + location.hash, + dimension1: + app.router.context && + app.router.context.doc && + app.router.context.doc.slug_without_version, + }); }); - }); - page.track(function() { - if (window._gauges) - _gauges.push(['track']); - else - (function() { - var _gauges=_gauges||[];!function(){var a=document.createElement("script"); - a.type="text/javascript",a.async=!0,a.id="gauges-tracker", - a.setAttribute("data-site-id","51c15f82613f5d7819000067"), - a.src="https://secure.gaug.es/track.js";var b=document.getElementsByTagName("script")[0]; - b.parentNode.insertBefore(a,b)}(); - })(); - }); + page.track(function () { + if (window._gauges) _gauges.push(["track"]); + else + (function () { + var _gauges = _gauges || []; + !(function () { + var a = document.createElement("script"); + (a.type = "text/javascript"), + (a.async = !0), + (a.id = "gauges-tracker"), + a.setAttribute("data-site-id", "51c15f82613f5d7819000067"), + (a.src = "https://secure.gaug.es/track.js"); + var b = document.getElementsByTagName("script")[0]; + b.parentNode.insertBefore(a, b); + })(); + })(); + }); + } else { + resetAnalytics(); + } } -} catch(e) { } +} catch (e) {} diff --git a/assets/javascripts/vendor/classlist.js b/assets/javascripts/vendor/classlist.js deleted file mode 100644 index fe9ca75d74..0000000000 --- a/assets/javascripts/vendor/classlist.js +++ /dev/null @@ -1,240 +0,0 @@ -/* - * classList.js: Cross-browser full element.classList implementation. - * 1.1.20170427 - * - * By Eli Grey, http://eligrey.com - * License: Dedicated to the public domain. - * See https://github.com/eligrey/classList.js/blob/master/LICENSE.md - */ - -/*global self, document, DOMException */ - -/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ - -if ("document" in self) { - -// Full polyfill for browsers with no classList support -// Including IE < Edge missing SVGElement.classList -if (!("classList" in document.createElement("_")) - || document.createElementNS && !("classList" in document.createElementNS("http://www.w3.org/2000/svg","g"))) { - -(function (view) { - -"use strict"; - -if (!('Element' in view)) return; - -var - classListProp = "classList" - , protoProp = "prototype" - , elemCtrProto = view.Element[protoProp] - , objCtr = Object - , strTrim = String[protoProp].trim || function () { - return this.replace(/^\s+|\s+$/g, ""); - } - , arrIndexOf = Array[protoProp].indexOf || function (item) { - var - i = 0 - , len = this.length - ; - for (; i < len; i++) { - if (i in this && this[i] === item) { - return i; - } - } - return -1; - } - // Vendors: please allow content code to instantiate DOMExceptions - , DOMEx = function (type, message) { - this.name = type; - this.code = DOMException[type]; - this.message = message; - } - , checkTokenAndGetIndex = function (classList, token) { - if (token === "") { - throw new DOMEx( - "SYNTAX_ERR" - , "An invalid or illegal string was specified" - ); - } - if (/\s/.test(token)) { - throw new DOMEx( - "INVALID_CHARACTER_ERR" - , "String contains an invalid character" - ); - } - return arrIndexOf.call(classList, token); - } - , ClassList = function (elem) { - var - trimmedClasses = strTrim.call(elem.getAttribute("class") || "") - , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] - , i = 0 - , len = classes.length - ; - for (; i < len; i++) { - this.push(classes[i]); - } - this._updateClassName = function () { - elem.setAttribute("class", this.toString()); - }; - } - , classListProto = ClassList[protoProp] = [] - , classListGetter = function () { - return new ClassList(this); - } -; -// Most DOMException implementations don't allow calling DOMException's toString() -// on non-DOMExceptions. Error's toString() is sufficient here. -DOMEx[protoProp] = Error[protoProp]; -classListProto.item = function (i) { - return this[i] || null; -}; -classListProto.contains = function (token) { - token += ""; - return checkTokenAndGetIndex(this, token) !== -1; -}; -classListProto.add = function () { - var - tokens = arguments - , i = 0 - , l = tokens.length - , token - , updated = false - ; - do { - token = tokens[i] + ""; - if (checkTokenAndGetIndex(this, token) === -1) { - this.push(token); - updated = true; - } - } - while (++i < l); - - if (updated) { - this._updateClassName(); - } -}; -classListProto.remove = function () { - var - tokens = arguments - , i = 0 - , l = tokens.length - , token - , updated = false - , index - ; - do { - token = tokens[i] + ""; - index = checkTokenAndGetIndex(this, token); - while (index !== -1) { - this.splice(index, 1); - updated = true; - index = checkTokenAndGetIndex(this, token); - } - } - while (++i < l); - - if (updated) { - this._updateClassName(); - } -}; -classListProto.toggle = function (token, force) { - token += ""; - - var - result = this.contains(token) - , method = result ? - force !== true && "remove" - : - force !== false && "add" - ; - - if (method) { - this[method](token); - } - - if (force === true || force === false) { - return force; - } else { - return !result; - } -}; -classListProto.toString = function () { - return this.join(" "); -}; - -if (objCtr.defineProperty) { - var classListPropDesc = { - get: classListGetter - , enumerable: true - , configurable: true - }; - try { - objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); - } catch (ex) { // IE 8 doesn't support enumerable:true - // adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36 - // modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected - if (ex.number === undefined || ex.number === -0x7FF5EC54) { - classListPropDesc.enumerable = false; - objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); - } - } -} else if (objCtr[protoProp].__defineGetter__) { - elemCtrProto.__defineGetter__(classListProp, classListGetter); -} - -}(self)); - -} - -// There is full or partial native classList support, so just check if we need -// to normalize the add/remove and toggle APIs. - -(function () { - "use strict"; - - var testElement = document.createElement("_"); - - testElement.classList.add("c1", "c2"); - - // Polyfill for IE 10/11 and Firefox <26, where classList.add and - // classList.remove exist but support only one argument at a time. - if (!testElement.classList.contains("c2")) { - var createMethod = function(method) { - var original = DOMTokenList.prototype[method]; - - DOMTokenList.prototype[method] = function(token) { - var i, len = arguments.length; - - for (i = 0; i < len; i++) { - token = arguments[i]; - original.call(this, token); - } - }; - }; - createMethod('add'); - createMethod('remove'); - } - - testElement.classList.toggle("c3", false); - - // Polyfill for IE 10 and Firefox <24, where classList.toggle does not - // support the second argument. - if (testElement.classList.contains("c3")) { - var _toggle = DOMTokenList.prototype.toggle; - - DOMTokenList.prototype.toggle = function(token, force) { - if (1 in arguments && !this.contains(token) === !force) { - return force; - } else { - return _toggle.call(this, token); - } - }; - - } - - testElement = null; -}()); - -} diff --git a/assets/javascripts/vendor/cookies.js b/assets/javascripts/vendor/cookies.js index 35617f7388..e592a5de8e 100644 --- a/assets/javascripts/vendor/cookies.js +++ b/assets/javascripts/vendor/cookies.js @@ -1,172 +1,208 @@ /* - * Cookies.js - 1.2.3 + * Cookies.js - 1.2.3 (patched for SameSite=Strict and secure=true) * https://github.com/ScottHamper/Cookies * * This is free and unencumbered software released into the public domain. */ (function (global, undefined) { - 'use strict'; + "use strict"; - var factory = function (window) { - if (typeof window.document !== 'object') { - throw new Error('Cookies.js requires a `window` with a `document` object'); + var factory = function (window) { + if (typeof window.document !== "object") { + throw new Error( + "Cookies.js requires a `window` with a `document` object", + ); + } + + var Cookies = function (key, value, options) { + return arguments.length === 1 + ? Cookies.get(key) + : Cookies.set(key, value, options); + }; + + // Allows for setter injection in unit tests + Cookies._document = window.document; + + // Used to ensure cookie keys do not collide with + // built-in `Object` properties + Cookies._cacheKeyPrefix = "cookey."; // Hurr hurr, :) + + Cookies._maxExpireDate = new Date("Fri, 31 Dec 9999 23:59:59 UTC"); + + Cookies.defaults = { + path: "/", + SameSite: "Strict", + secure: true, + }; + + Cookies.get = function (key) { + if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) { + Cookies._renewCache(); + } + + var value = Cookies._cache[Cookies._cacheKeyPrefix + key]; + + return value === undefined ? undefined : decodeURIComponent(value); + }; + + Cookies.set = function (key, value, options) { + options = Cookies._getExtendedOptions(options); + options.expires = Cookies._getExpiresDate( + value === undefined ? -1 : options.expires, + ); + + Cookies._document.cookie = Cookies._generateCookieString( + key, + value, + options, + ); + + return Cookies; + }; + + Cookies.expire = function (key, options) { + return Cookies.set(key, undefined, options); + }; + + Cookies._getExtendedOptions = function (options) { + return { + path: (options && options.path) || Cookies.defaults.path, + domain: (options && options.domain) || Cookies.defaults.domain, + SameSite: (options && options.SameSite) || Cookies.defaults.SameSite, + expires: (options && options.expires) || Cookies.defaults.expires, + secure: + options && options.secure !== undefined + ? options.secure + : Cookies.defaults.secure, + }; + }; + + Cookies._isValidDate = function (date) { + return ( + Object.prototype.toString.call(date) === "[object Date]" && + !isNaN(date.getTime()) + ); + }; + + Cookies._getExpiresDate = function (expires, now) { + now = now || new Date(); + + if (typeof expires === "number") { + expires = + expires === Infinity + ? Cookies._maxExpireDate + : new Date(now.getTime() + expires * 1000); + } else if (typeof expires === "string") { + expires = new Date(expires); + } + + if (expires && !Cookies._isValidDate(expires)) { + throw new Error( + "`expires` parameter cannot be converted to a valid Date instance", + ); + } + + return expires; + }; + + Cookies._generateCookieString = function (key, value, options) { + key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent); + key = key.replace(/\(/g, "%28").replace(/\)/g, "%29"); + value = (value + "").replace( + /[^!#$&-+\--:<-\[\]-~]/g, + encodeURIComponent, + ); + options = options || {}; + + var cookieString = key + "=" + value; + cookieString += options.path ? ";path=" + options.path : ""; + cookieString += options.domain ? ";domain=" + options.domain : ""; + cookieString += options.SameSite ? ";SameSite=" + options.SameSite : ""; + cookieString += options.expires + ? ";expires=" + options.expires.toUTCString() + : ""; + cookieString += options.secure ? ";secure" : ""; + + return cookieString; + }; + + Cookies._getCacheFromString = function (documentCookie) { + var cookieCache = {}; + var cookiesArray = documentCookie ? documentCookie.split("; ") : []; + + for (var i = 0; i < cookiesArray.length; i++) { + var cookieKvp = Cookies._getKeyValuePairFromCookieString( + cookiesArray[i], + ); + + if ( + cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined + ) { + cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = + cookieKvp.value; } + } - var Cookies = function (key, value, options) { - return arguments.length === 1 ? - Cookies.get(key) : Cookies.set(key, value, options); - }; + return cookieCache; + }; - // Allows for setter injection in unit tests - Cookies._document = window.document; + Cookies._getKeyValuePairFromCookieString = function (cookieString) { + // "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')` + var separatorIndex = cookieString.indexOf("="); + + // IE omits the "=" when the cookie value is an empty string + separatorIndex = + separatorIndex < 0 ? cookieString.length : separatorIndex; + + var key = cookieString.substr(0, separatorIndex); + var decodedKey; + try { + decodedKey = decodeURIComponent(key); + } catch (e) { + if (console && typeof console.error === "function") { + console.error('Could not decode cookie with key "' + key + '"', e); + } + } - // Used to ensure cookie keys do not collide with - // built-in `Object` properties - Cookies._cacheKeyPrefix = 'cookey.'; // Hurr hurr, :) + return { + key: decodedKey, + value: cookieString.substr(separatorIndex + 1), // Defer decoding value until accessed + }; + }; - Cookies._maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC'); + Cookies._renewCache = function () { + Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie); + Cookies._cachedDocumentCookie = Cookies._document.cookie; + }; - Cookies.defaults = { - path: '/', - secure: false - }; - - Cookies.get = function (key) { - if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) { - Cookies._renewCache(); - } - - var value = Cookies._cache[Cookies._cacheKeyPrefix + key]; - - return value === undefined ? undefined : decodeURIComponent(value); - }; - - Cookies.set = function (key, value, options) { - options = Cookies._getExtendedOptions(options); - options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires); - - Cookies._document.cookie = Cookies._generateCookieString(key, value, options); - - return Cookies; - }; - - Cookies.expire = function (key, options) { - return Cookies.set(key, undefined, options); - }; - - Cookies._getExtendedOptions = function (options) { - return { - path: options && options.path || Cookies.defaults.path, - domain: options && options.domain || Cookies.defaults.domain, - expires: options && options.expires || Cookies.defaults.expires, - secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure - }; - }; - - Cookies._isValidDate = function (date) { - return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime()); - }; - - Cookies._getExpiresDate = function (expires, now) { - now = now || new Date(); - - if (typeof expires === 'number') { - expires = expires === Infinity ? - Cookies._maxExpireDate : new Date(now.getTime() + expires * 1000); - } else if (typeof expires === 'string') { - expires = new Date(expires); - } - - if (expires && !Cookies._isValidDate(expires)) { - throw new Error('`expires` parameter cannot be converted to a valid Date instance'); - } - - return expires; - }; - - Cookies._generateCookieString = function (key, value, options) { - key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent); - key = key.replace(/\(/g, '%28').replace(/\)/g, '%29'); - value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent); - options = options || {}; - - var cookieString = key + '=' + value; - cookieString += options.path ? ';path=' + options.path : ''; - cookieString += options.domain ? ';domain=' + options.domain : ''; - cookieString += options.expires ? ';expires=' + options.expires.toUTCString() : ''; - cookieString += options.secure ? ';secure' : ''; - - return cookieString; - }; - - Cookies._getCacheFromString = function (documentCookie) { - var cookieCache = {}; - var cookiesArray = documentCookie ? documentCookie.split('; ') : []; - - for (var i = 0; i < cookiesArray.length; i++) { - var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]); - - if (cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined) { - cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = cookieKvp.value; - } - } - - return cookieCache; - }; - - Cookies._getKeyValuePairFromCookieString = function (cookieString) { - // "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')` - var separatorIndex = cookieString.indexOf('='); - - // IE omits the "=" when the cookie value is an empty string - separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex; - - var key = cookieString.substr(0, separatorIndex); - var decodedKey; - try { - decodedKey = decodeURIComponent(key); - } catch (e) { - if (console && typeof console.error === 'function') { - console.error('Could not decode cookie with key "' + key + '"', e); - } - } - - return { - key: decodedKey, - value: cookieString.substr(separatorIndex + 1) // Defer decoding value until accessed - }; - }; - - Cookies._renewCache = function () { - Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie); - Cookies._cachedDocumentCookie = Cookies._document.cookie; - }; - - Cookies._areEnabled = function () { - var testKey = 'cookies.js'; - var areEnabled = Cookies.set(testKey, 1).get(testKey) === '1'; - Cookies.expire(testKey); - return areEnabled; - }; - - Cookies.enabled = Cookies._areEnabled(); - - return Cookies; + Cookies._areEnabled = function () { + var testKey = "cookies.js"; + var areEnabled = Cookies.set(testKey, 1).get(testKey) === "1"; + Cookies.expire(testKey); + return areEnabled; }; - var cookiesExport = (global && typeof global.document === 'object') ? factory(global) : factory; - // AMD support - if (typeof define === 'function' && define.amd) { - define(function () { return cookiesExport; }); + Cookies.enabled = Cookies._areEnabled(); + + return Cookies; + }; + var cookiesExport = + global && typeof global.document === "object" ? factory(global) : factory; + + // AMD support + if (typeof define === "function" && define.amd) { + define(function () { + return cookiesExport; + }); // CommonJS/Node.js support - } else if (typeof exports === 'object') { - // Support Node.js specific `module.exports` (which can be a function) - if (typeof module === 'object' && typeof module.exports === 'object') { - exports = module.exports = cookiesExport; - } - // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) - exports.Cookies = cookiesExport; - } else { - global.Cookies = cookiesExport; + } else if (typeof exports === "object") { + // Support Node.js specific `module.exports` (which can be a function) + if (typeof module === "object" && typeof module.exports === "object") { + exports = module.exports = cookiesExport; } -})(typeof window === 'undefined' ? this : window); + // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) + exports.Cookies = cookiesExport; + } else { + global.Cookies = cookiesExport; + } +})(typeof window === "undefined" ? this : window); diff --git a/assets/javascripts/vendor/fastclick.js b/assets/javascripts/vendor/fastclick.js deleted file mode 100755 index 3af4f9d6f1..0000000000 --- a/assets/javascripts/vendor/fastclick.js +++ /dev/null @@ -1,841 +0,0 @@ -;(function () { - 'use strict'; - - /** - * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. - * - * @codingstandard ftlabs-jsv2 - * @copyright The Financial Times Limited [All Rights Reserved] - * @license MIT License (see LICENSE.txt) - */ - - /*jslint browser:true, node:true*/ - /*global define, Event, Node*/ - - - /** - * Instantiate fast-clicking listeners on the specified layer. - * - * @constructor - * @param {Element} layer The layer to listen on - * @param {Object} [options={}] The options to override the defaults - */ - function FastClick(layer, options) { - var oldOnClick; - - options = options || {}; - - /** - * Whether a click is currently being tracked. - * - * @type boolean - */ - this.trackingClick = false; - - - /** - * Timestamp for when click tracking started. - * - * @type number - */ - this.trackingClickStart = 0; - - - /** - * The element being tracked for a click. - * - * @type EventTarget - */ - this.targetElement = null; - - - /** - * X-coordinate of touch start event. - * - * @type number - */ - this.touchStartX = 0; - - - /** - * Y-coordinate of touch start event. - * - * @type number - */ - this.touchStartY = 0; - - - /** - * ID of the last touch, retrieved from Touch.identifier. - * - * @type number - */ - this.lastTouchIdentifier = 0; - - - /** - * Touchmove boundary, beyond which a click will be cancelled. - * - * @type number - */ - this.touchBoundary = options.touchBoundary || 10; - - - /** - * The FastClick layer. - * - * @type Element - */ - this.layer = layer; - - /** - * The minimum time between tap(touchstart and touchend) events - * - * @type number - */ - this.tapDelay = options.tapDelay || 200; - - /** - * The maximum time for a tap - * - * @type number - */ - this.tapTimeout = options.tapTimeout || 700; - - if (FastClick.notNeeded(layer)) { - return; - } - - // Some old versions of Android don't have Function.prototype.bind - function bind(method, context) { - return function() { return method.apply(context, arguments); }; - } - - - var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel']; - var context = this; - for (var i = 0, l = methods.length; i < l; i++) { - context[methods[i]] = bind(context[methods[i]], context); - } - - // Set up event handlers as required - if (deviceIsAndroid) { - layer.addEventListener('mouseover', this.onMouse, true); - layer.addEventListener('mousedown', this.onMouse, true); - layer.addEventListener('mouseup', this.onMouse, true); - } - - layer.addEventListener('click', this.onClick, true); - layer.addEventListener('touchstart', this.onTouchStart, false); - layer.addEventListener('touchmove', this.onTouchMove, false); - layer.addEventListener('touchend', this.onTouchEnd, false); - layer.addEventListener('touchcancel', this.onTouchCancel, false); - - // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) - // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick - // layer when they are cancelled. - if (!Event.prototype.stopImmediatePropagation) { - layer.removeEventListener = function(type, callback, capture) { - var rmv = Node.prototype.removeEventListener; - if (type === 'click') { - rmv.call(layer, type, callback.hijacked || callback, capture); - } else { - rmv.call(layer, type, callback, capture); - } - }; - - layer.addEventListener = function(type, callback, capture) { - var adv = Node.prototype.addEventListener; - if (type === 'click') { - adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { - if (!event.propagationStopped) { - callback(event); - } - }), capture); - } else { - adv.call(layer, type, callback, capture); - } - }; - } - - // If a handler is already declared in the element's onclick attribute, it will be fired before - // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and - // adding it as listener. - if (typeof layer.onclick === 'function') { - - // Android browser on at least 3.2 requires a new reference to the function in layer.onclick - // - the old one won't work if passed to addEventListener directly. - oldOnClick = layer.onclick; - layer.addEventListener('click', function(event) { - oldOnClick(event); - }, false); - layer.onclick = null; - } - } - - /** - * Windows Phone 8.1 fakes user agent string to look like Android and iPhone. - * - * @type boolean - */ - var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0; - - /** - * Android requires exceptions. - * - * @type boolean - */ - var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone; - - - /** - * iOS requires exceptions. - * - * @type boolean - */ - var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone; - - - /** - * iOS 4 requires an exception for select elements. - * - * @type boolean - */ - var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); - - - /** - * iOS 6.0-7.* requires the target element to be manually derived - * - * @type boolean - */ - var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent); - - /** - * BlackBerry requires exceptions. - * - * @type boolean - */ - var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0; - - /** - * Determine whether a given element requires a native click. - * - * @param {EventTarget|Element} target Target DOM element - * @returns {boolean} Returns true if the element needs a native click - */ - FastClick.prototype.needsClick = function(target) { - switch (target.nodeName.toLowerCase()) { - - // Don't send a synthetic click to disabled inputs (issue #62) - case 'button': - case 'select': - case 'textarea': - if (target.disabled) { - return true; - } - - break; - case 'input': - - // File inputs need real clicks on iOS 6 due to a browser bug (issue #68) - if ((deviceIsIOS && target.type === 'file') || target.disabled) { - return true; - } - - break; - case 'label': - case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames - case 'video': - return true; - } - - return (/\bneedsclick\b/).test(target.className); - }; - - - /** - * Determine whether a given element requires a call to focus to simulate click into element. - * - * @param {EventTarget|Element} target Target DOM element - * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. - */ - FastClick.prototype.needsFocus = function(target) { - switch (target.nodeName.toLowerCase()) { - case 'textarea': - return true; - case 'select': - return !deviceIsAndroid; - case 'input': - switch (target.type) { - case 'button': - case 'checkbox': - case 'file': - case 'image': - case 'radio': - case 'submit': - return false; - } - - // No point in attempting to focus disabled inputs - return !target.disabled && !target.readOnly; - default: - return (/\bneedsfocus\b/).test(target.className); - } - }; - - - /** - * Send a click event to the specified element. - * - * @param {EventTarget|Element} targetElement - * @param {Event} event - */ - FastClick.prototype.sendClick = function(targetElement, event) { - var clickEvent, touch; - - // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) - if (document.activeElement && document.activeElement !== targetElement) { - document.activeElement.blur(); - } - - touch = event.changedTouches[0]; - - // Synthesise a click event, with an extra attribute so it can be tracked - clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); - clickEvent.forwardedTouchEvent = true; - targetElement.dispatchEvent(clickEvent); - }; - - FastClick.prototype.determineEventType = function(targetElement) { - - //Issue #159: Android Chrome Select Box does not open with a synthetic click event - if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { - return 'mousedown'; - } - - return 'click'; - }; - - - /** - * @param {EventTarget|Element} targetElement - */ - FastClick.prototype.focus = function(targetElement) { - var length; - - // Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. - if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') { - length = targetElement.value.length; - targetElement.setSelectionRange(length, length); - } else { - targetElement.focus(); - } - }; - - - /** - * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it. - * - * @param {EventTarget|Element} targetElement - */ - FastClick.prototype.updateScrollParent = function(targetElement) { - var scrollParent, parentElement; - - scrollParent = targetElement.fastClickScrollParent; - - // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the - // target element was moved to another parent. - if (!scrollParent || !scrollParent.contains(targetElement)) { - parentElement = targetElement; - do { - if (parentElement.scrollHeight > parentElement.offsetHeight) { - scrollParent = parentElement; - targetElement.fastClickScrollParent = parentElement; - break; - } - - parentElement = parentElement.parentElement; - } while (parentElement); - } - - // Always update the scroll top tracker if possible. - if (scrollParent) { - scrollParent.fastClickLastScrollTop = scrollParent.scrollTop; - } - }; - - - /** - * @param {EventTarget} targetElement - * @returns {Element|EventTarget} - */ - FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { - - // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node. - if (eventTarget.nodeType === Node.TEXT_NODE) { - return eventTarget.parentNode; - } - - return eventTarget; - }; - - - /** - * On touch start, record the position and scroll offset. - * - * @param {Event} event - * @returns {boolean} - */ - FastClick.prototype.onTouchStart = function(event) { - var targetElement, touch, selection; - - // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111). - if (event.targetTouches.length > 1) { - return true; - } - - targetElement = this.getTargetElementFromEventTarget(event.target); - touch = event.targetTouches[0]; - - if (deviceIsIOS) { - - // Only trusted events will deselect text on iOS (issue #49) - selection = window.getSelection(); - if (selection.rangeCount && !selection.isCollapsed) { - return true; - } - - if (!deviceIsIOS4) { - - // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): - // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched - // with the same identifier as the touch event that previously triggered the click that triggered the alert. - // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an - // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform. - // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string, - // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long, - // random integers, it's safe to to continue if the identifier is 0 here. - if (touch.identifier && touch.identifier === this.lastTouchIdentifier) { - event.preventDefault(); - return false; - } - - this.lastTouchIdentifier = touch.identifier; - - // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: - // 1) the user does a fling scroll on the scrollable layer - // 2) the user stops the fling scroll with another tap - // then the event.target of the last 'touchend' event will be the element that was under the user's finger - // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check - // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). - this.updateScrollParent(targetElement); - } - } - - this.trackingClick = true; - this.trackingClickStart = event.timeStamp; - this.targetElement = targetElement; - - this.touchStartX = touch.pageX; - this.touchStartY = touch.pageY; - - // Prevent phantom clicks on fast double-tap (issue #36) - if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { - event.preventDefault(); - } - - return true; - }; - - - /** - * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. - * - * @param {Event} event - * @returns {boolean} - */ - FastClick.prototype.touchHasMoved = function(event) { - var touch = event.changedTouches[0], boundary = this.touchBoundary; - - if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) { - return true; - } - - return false; - }; - - - /** - * Update the last position. - * - * @param {Event} event - * @returns {boolean} - */ - FastClick.prototype.onTouchMove = function(event) { - if (!this.trackingClick) { - return true; - } - - // If the touch has moved, cancel the click tracking - if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { - this.trackingClick = false; - this.targetElement = null; - } - - return true; - }; - - - /** - * Attempt to find the labelled control for the given label element. - * - * @param {EventTarget|HTMLLabelElement} labelElement - * @returns {Element|null} - */ - FastClick.prototype.findControl = function(labelElement) { - - // Fast path for newer browsers supporting the HTML5 control attribute - if (labelElement.control !== undefined) { - return labelElement.control; - } - - // All browsers under test that support touch events also support the HTML5 htmlFor attribute - if (labelElement.htmlFor) { - return document.getElementById(labelElement.htmlFor); - } - - // If no for attribute exists, attempt to retrieve the first labellable descendant element - // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label - return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); - }; - - - /** - * On touch end, determine whether to send a click event at once. - * - * @param {Event} event - * @returns {boolean} - */ - FastClick.prototype.onTouchEnd = function(event) { - var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; - - if (!this.trackingClick) { - return true; - } - - // Prevent phantom clicks on fast double-tap (issue #36) - if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { - this.cancelNextClick = true; - return true; - } - - if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) { - return true; - } - - // Reset to prevent wrong click cancel on input (issue #156). - this.cancelNextClick = false; - - this.lastClickTime = event.timeStamp; - - trackingClickStart = this.trackingClickStart; - this.trackingClick = false; - this.trackingClickStart = 0; - - // On some iOS devices, the targetElement supplied with the event is invalid if the layer - // is performing a transition or scroll, and has to be re-detected manually. Note that - // for this to function correctly, it must be called *after* the event target is checked! - // See issue #57; also filed as rdar://13048589 . - if (deviceIsIOSWithBadTarget) { - touch = event.changedTouches[0]; - - // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null - targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; - targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; - } - - targetTagName = targetElement.tagName.toLowerCase(); - if (targetTagName === 'label') { - forElement = this.findControl(targetElement); - if (forElement) { - this.focus(targetElement); - if (deviceIsAndroid) { - return false; - } - - targetElement = forElement; - } - } else if (this.needsFocus(targetElement)) { - - // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. - // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). - if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { - this.targetElement = null; - return false; - } - - this.focus(targetElement); - this.sendClick(targetElement, event); - - // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. - // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others) - if (!deviceIsIOS || targetTagName !== 'select') { - this.targetElement = null; - event.preventDefault(); - } - - return false; - } - - if (deviceIsIOS && !deviceIsIOS4) { - - // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled - // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). - scrollParent = targetElement.fastClickScrollParent; - if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { - return true; - } - } - - // Prevent the actual click from going though - unless the target node is marked as requiring - // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. - if (!this.needsClick(targetElement)) { - event.preventDefault(); - this.sendClick(targetElement, event); - } - - return false; - }; - - - /** - * On touch cancel, stop tracking the click. - * - * @returns {void} - */ - FastClick.prototype.onTouchCancel = function() { - this.trackingClick = false; - this.targetElement = null; - }; - - - /** - * Determine mouse events which should be permitted. - * - * @param {Event} event - * @returns {boolean} - */ - FastClick.prototype.onMouse = function(event) { - - // If a target element was never set (because a touch event was never fired) allow the event - if (!this.targetElement) { - return true; - } - - if (event.forwardedTouchEvent) { - return true; - } - - // Programmatically generated events targeting a specific element should be permitted - if (!event.cancelable) { - return true; - } - - // Derive and check the target element to see whether the mouse event needs to be permitted; - // unless explicitly enabled, prevent non-touch click events from triggering actions, - // to prevent ghost/doubleclicks. - if (!this.needsClick(this.targetElement) || this.cancelNextClick) { - - // Prevent any user-added listeners declared on FastClick element from being fired. - if (event.stopImmediatePropagation) { - event.stopImmediatePropagation(); - } else { - - // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) - event.propagationStopped = true; - } - - // Cancel the event - event.stopPropagation(); - event.preventDefault(); - - return false; - } - - // If the mouse event is permitted, return true for the action to go through. - return true; - }; - - - /** - * On actual clicks, determine whether this is a touch-generated click, a click action occurring - * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or - * an actual click which should be permitted. - * - * @param {Event} event - * @returns {boolean} - */ - FastClick.prototype.onClick = function(event) { - var permitted; - - // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. - if (this.trackingClick) { - this.targetElement = null; - this.trackingClick = false; - return true; - } - - // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. - if (event.target.type === 'submit' && event.detail === 0) { - return true; - } - - permitted = this.onMouse(event); - - // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through. - if (!permitted) { - this.targetElement = null; - } - - // If clicks are permitted, return true for the action to go through. - return permitted; - }; - - - /** - * Remove all FastClick's event listeners. - * - * @returns {void} - */ - FastClick.prototype.destroy = function() { - var layer = this.layer; - - if (deviceIsAndroid) { - layer.removeEventListener('mouseover', this.onMouse, true); - layer.removeEventListener('mousedown', this.onMouse, true); - layer.removeEventListener('mouseup', this.onMouse, true); - } - - layer.removeEventListener('click', this.onClick, true); - layer.removeEventListener('touchstart', this.onTouchStart, false); - layer.removeEventListener('touchmove', this.onTouchMove, false); - layer.removeEventListener('touchend', this.onTouchEnd, false); - layer.removeEventListener('touchcancel', this.onTouchCancel, false); - }; - - - /** - * Check whether FastClick is needed. - * - * @param {Element} layer The layer to listen on - */ - FastClick.notNeeded = function(layer) { - var metaViewport; - var chromeVersion; - var blackberryVersion; - var firefoxVersion; - - // Devices that don't support touch don't need FastClick - if (typeof window.ontouchstart === 'undefined') { - return true; - } - - // Chrome version - zero for other browsers - chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; - - if (chromeVersion) { - - if (deviceIsAndroid) { - metaViewport = document.querySelector('meta[name=viewport]'); - - if (metaViewport) { - // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) - if (metaViewport.content.indexOf('user-scalable=no') !== -1) { - return true; - } - // Chrome 32 and above with width=device-width or less don't need FastClick - if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) { - return true; - } - } - - // Chrome desktop doesn't need FastClick (issue #15) - } else { - return true; - } - } - - if (deviceIsBlackBerry10) { - blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/); - - // BlackBerry 10.3+ does not require Fastclick library. - // https://github.com/ftlabs/fastclick/issues/251 - if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) { - metaViewport = document.querySelector('meta[name=viewport]'); - - if (metaViewport) { - // user-scalable=no eliminates click delay. - if (metaViewport.content.indexOf('user-scalable=no') !== -1) { - return true; - } - // width=device-width (or less than device-width) eliminates click delay. - if (document.documentElement.scrollWidth <= window.outerWidth) { - return true; - } - } - } - } - - // IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97) - if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') { - return true; - } - - // Firefox version - zero for other browsers - firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; - - if (firefoxVersion >= 27) { - // Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896 - - metaViewport = document.querySelector('meta[name=viewport]'); - if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) { - return true; - } - } - - // IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version - // http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx - if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') { - return true; - } - - return false; - }; - - - /** - * Factory method for creating a FastClick object - * - * @param {Element} layer The layer to listen on - * @param {Object} [options={}] The options to override the defaults - */ - FastClick.attach = function(layer, options) { - return new FastClick(layer, options); - }; - - - if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { - - // AMD. Register as an anonymous module. - define(function() { - return FastClick; - }); - } else if (typeof module !== 'undefined' && module.exports) { - module.exports = FastClick.attach; - module.exports.FastClick = FastClick; - } else { - window.FastClick = FastClick; - } -}()); diff --git a/assets/javascripts/vendor/mathml.js b/assets/javascripts/vendor/mathml.js index b97829581c..129726201c 100644 --- a/assets/javascripts/vendor/mathml.js +++ b/assets/javascripts/vendor/mathml.js @@ -4,17 +4,22 @@ * Adapted from: https://github.com/fred-wang/mathml.css */ (function () { - window.addEventListener("load", function() { + window.addEventListener("load", function () { var box, div, link, namespaceURI; // First check whether the page contains any