# Technical Documentation: matomo-org/matomo

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/matomo-org/matomo)

### Overview (`README.md`)

# Matomo (formerly Piwik) - matomo.org

[](https://matomo.org/download/)
[](https://builds.matomo.org/)
[](https://matomo.org/free-software/)

## Code Status

[](https://github.com/matomo-org/matomo/actions/workflows/matomo-tests.yml)
[](http://isitmaintained.com/project/matomo-org/matomo "Percentage of issues still open")

## Description

Matomo is the leading Free/Libre open analytics platform.

Matomo is a full-featured PHP MySQL software program that you download and install on your own webserver.
At the end of the five-minute installation process, you will be given a JavaScript code.
Simply copy and paste this tag on websites you wish to track and access your analytics reports in real-time.

Matomo aims to be a Free software alternative to Google Analytics and is already used on more than 1,400,000 websites. Privacy is built-in!

## Mission Statement

> « To create, as a community, the leading international open source digital analytics platform, that gives every user full control of their data, to empower better decisions. »

Our purpose is to:

> « Empower People Ethically »

## License

Matomo is released under the GPL v3 (or later) license, see [LICENSE](LICENSE).

## Requirements

  * PHP 8.1.0 or greater
  * MySQL version 8.0 or greater, or MariaDB version 10.6 or greater
  * PHP extension pdo and pdo_mysql, or the MySQLi extension
  * Matomo is OS / server independent

See https://matomo.org/docs/requirements/.

## Install Matomo

  * [Download Matomo](https://matomo.org/download/)
  * Upload Matomo to your webserver
  * Point your browser to the directory
  * Follow the steps
  * Add the given JavaScript code to your pages
  * (You may also generate fake data to experiment, by enabling the plugin VisitorGenerator)

See https://matomo.org/docs/installation/.

## Develop for Matomo

When using Matomo for development you need to [install Matomo from the Git repository](https://matomo.org/faq/how-to-install/faq_18271/).

This will also give you access to a DDEV environment you can use. More details can be found in the [DDEV README](.ddev/README.md).

## Free trial 

If you do not have a server or don't want to host yourself, you can use our Matomo Cloud service (21 day free trial): https://matomo.org/start-free-analytics-trial/

## Online Demo

Check out the online demo for Matomo at [demo.matomo.cloud](https://demo.matomo.cloud/).

## Changelog

For the list of all tickets closed in the current and past releases, see [matomo.org/changelog/](https://matomo.org/changelog/). For the list of technical changes in the Matomo platform, see [developer.matomo.org/changelog](https://developer.matomo.org/changelog).

## Get involved!

We believe in liberating Web Analytics, providing a free platform for simple and advanced analytics. Matomo was built by dozens of people like you,
and we need your help to make Matomo better… Why not participate in a useful project today? [Learn how you can contribute to Matomo](https://matomo.org/get-involved).

## Careers at Matomo

We're hiring! Learn more on our [Careers page](https://matomo.org/jobs/).

## Translations

Our translations are managed on [Weblate](https://hosted.weblate.org/engage/matomo/).

[](https://hosted.weblate.org/engage/matomo/)

## Quality Assurance

The Matomo project uses an ever-expanding comprehensive set of thousands of unit tests and hundreds of automated integration tests, system tests, JavaScript tests, and screenshot UI tests, running on a continuous integration server as part of its software quality assurance. [Learn more](https://developer.matomo.org/guides/tests).

We use [BrowserStack.com](https://www.browserstack.com/) testing tool to help check the Matomo user interface is compatible with many browsers.

## Security

Security is a top priority at Matomo. As potential issues are discovered, we validate, patch and release fixes as quickly as we can. We have a security bug bounty program in place that rewards researchers for finding security issues and disclosing them to us. 

[Learn more](https://matomo.org/security/) or check out our [HackerOne program](https://hackerone.com/matomo).

## Support for Matomo

For **Free support**, post a message in our community forums: [forum.matomo.org](https://forum.matomo.org/)

For **Professional paid support**, purchase a Matomo On-Premise Support Plan: [matomo.org/support-plans](https://matomo.org/support-plans/)  

## Contact

Website: [matomo.org](https://matomo.org)

About us: [matomo.org/team/](https://matomo.org/team/)

Contact us: [matomo.org/contact/](https://matomo.org/contact/)

## More information

What makes Matomo unique from the competition:

  * You own your web analytics data: since Matomo is installed on your server, the data is stored in your own database and you can get all the statistics using the powerful Matomo Analytics API.

  * Matomo is a Free Software which can easily be configured to respect your visitors' privacy.

  * Modern, easy to use User Interface: you can fully customize your dashboard, drag and drop widgets and more.

  * Matomo features are built inside plugins: you can add new features and remove the ones you don’t need.
    You can build your own web analytics plugins or hire a consultant to have your custom feature built-in Matomo.

  * A vibrant international Open community of more than 200,000 active users (tracking even more websites!)

  * Advanced Web Analytics capabilities such as E-commerce Tracking, Goal tracking, Campaign tracking,
    Custom Variables, Email Reports, Custom Segment Editor, Geo Location, Real-time visits and maps, [and a lot more!](https://matomo.org/feature-overview/)

Documentation and more info on https://matomo.org.

We are together creating the best open analytics platform in the world!

---

### Overview (`.ddev/README.md`)

# Matomo DDEV environment

**WARNING**: Matomo DDEV environment can be helpful when developing for Matomo, to be able to create or test new plugins locally, to run unit, integration, system or UI tests, but it is **highly discouraged** to use DDEV environments to run or host publicly accessible production installations of Matomo.

## Prerequisites

Before you begin, make sure you have DDEV installed. If you don't have it yet, follow the installation instructions in the official [DDEV documentation](https://ddev.readthedocs.io/en/stable/).

If you don't have a Docker provider already available, you will need to start by choosing one. We recommend installing [Rancher Desktop](https://rancherdesktop.io/), as it is free and open source.

By default, Rancher Desktop will use ports 80 and 443 for Traefik router for Kubernetes. To make these ports available to DDEV, we recommend disabling Traefik by unchecking the checkbox: Preferences > Kubernetes > Enable Traefik.

## Setup steps

### 1. Start the environment

Once DDEV is installed, navigate to your project directory and run:

```
ddev start
```

This command will start the DDEV environment for local development. By default, your local environment will be accessible at [https://matomo.ddev.site](https://matomo.ddev.site).

You can also open this URL directly in your browser by running:

```
ddev launch
```

This will automatically open the default browser and take you to the Matomo setup page.

### 2. Set up Matomo using the UI installer

Follow the on-screen instructions in the UI installer to complete the Matomo setup. This will configure the necessary database and settings for your local instance.

### 3. Set up the development environment

After Matomo is set up, you can initialize the development environment by running:

```
ddev matomo:init:dev
```

This command will set up the environment for development, installing the additional dependencies required.

We have also added a parameter to the `ddev matomo:init:dev` command to enable source maps for Vue components. 
To enable them, you can run:
```
ddev matomo:init:dev --with-sourcemaps
```

You can also disable sourcemaps at any time if you want to, just run the default command again:
```
ddev matomo:init:dev
```
NOTE: You should tick 'Disable cache' on your browser developer tools to see the changes.

To see help:
```
ddev help matomo:init:dev
```

### 4. Set up the testing environment

After Matomo is set up, you can initialize the testing environment by running:

```
ddev matomo:init:tests
```

This command will set up the environment for running tests, ensuring everything is in place for the UI and other automated tests.

## Usage

The command `ddev matomo:console` provides access to all Matomo console commands. Some useful commands include:

- `core:archive` – Run local archiving
- `generate:plugin` - Generate a new plugin/theme including all needed files
- `tests:run` – Run unit, integration, and system tests
- `tests:run-ui` – Run UI tests
- `tests:run-js` – Run tracker JavaScript tests
- `tests:run-vue` – Run Vue component unit tests
- `cache:clear` - Remove all caches, including CSS and JavaScript
- `vue:build` - Builds vue modules for one or more plugins

To mount local plugin repositories into Matomo without symlinks, use the host commands:

```
ddev matomo:plugins:mount
ddev matomo:plugins:mount ../plugin-FormAnalytics ../plugin-HeatmapSessionRecording
ddev matomo:plugins:mount ../ '^.*/plugin-(FormAnalytics|Funnels)$'
ddev matomo:plugins:unmount FormAnalytics
ddev matomo:plugins:unmount
```

`ddev matomo:plugins:mount` creates a local-only `.ddev/docker-compose.local-plugins.yaml`, adds bind mounts for discovered `plugin-*` directories, and restarts DDEV automatically. If you pass a plugin directory path directly, the command reads the plugin name from `plugin.json` when present and otherwise infers it from directory names like `plugin-FormAnalytics`.

When a plugin declares `require.php` in `plugin.json`, the mount command compares it with the PHP version currently used by DDEV. Simple constraints such as `>=7.2.0` are enforced and incompatible plugins are skipped. More complex Composer-style expressions are only warned about and the plugin is mounted anyway.

`ddev matomo:plugins:unmount` removes one or more managed plugin mounts by plugin name. If all managed mounts are removed, the generated compose override is deleted automatically.

For more information about Matomo development, check out the official [Matomo Developer Documentation](https://developer.matomo.org/).

## Syncing test files from CI

When a build produces different UI screenshots or system test files than your checkout expects, you can copy the files CI generated back into your working copy:

```
ddev matomo:artifacts:sync-screenshots 12345 'Marketplace_.*'
ddev matomo:artifacts:sync-system-tests 12345 -e
```

The first argument is the build number, and both commands accept `-r` to sync a plugin repository, eg `-r innocraft/plugin-FormAnalytics`.

Syncing from `matomo-org/matomo` needs no setup, because core's own artifacts are public. Every other repository is protected, plugin repositories under `matomo-org` included, so the commands look up credentials whenever you point them somewhere else with `-r`. The credentials come from git, so they are stored wherever your credential helper keeps them and never appear in a file in the repository, in the process list or in your shell history — the password is handed to the console over STDIN.

Store them once per machine:

```
ddev matomo:artifacts:login
```

Which credential helper git uses is your own configuration, exactly as it is for `git push`. Pick one that keeps the password in your operating system's credential store rather than in a file:

```
git config --global credential.helper osxkeychain   # macOS
git config --global credential.helper manager       # Windows
git config --global credential.helper libsecret     # Linux
```

Git ships the helpers for macOS and Windows, so nothing needs installing there. On Debian and Ubuntu the libsecret helper ships as source in `/usr/share/doc/git/contrib/credential/libsecret` and has to be built once. Inside WSL2, point git at the Windows credential manager instead. Note that `git config --global credential.helper store` writes `~/.git-credentials` in plain text, so it is a poor choice for this.

Without a helper configured, git accepts the credentials and silently stores nothing, so `ddev matomo:artifacts:login` reads them back afterwards and tells you if that happened rather than reporting a success you did not get.

These host commands are covered by `tests/shell/ddev-artifacts-commands.sh`, which needs neither DDEV nor credentials to run and is run by CI on Linux, macOS and Windows.

The console commands behind these wrappers, `tests:sync-ui-screenshots` and `development:sync-system-test-processed`, can also be run through `ddev matomo:console` directly. They take the password as `--http-password`, or read it from STDIN with `--http-password-stdin`.

## Update PHP or MySQL version

You can create `.ddev/config.local.yaml` to adjust the environment configuration. This file will be automatically ignored by git.

For example, you can adjust `php_version` to `8.4`. All configuration from `.ddev/config.yaml` can be overridden. Check [DDEV documentation](https://docs.ddev.com/en/stable/users/configuration/config/#managing-configuration) for more details.

From your host, run `ddev restart` for the changes to take effect.

## Generating testing data locally

To see some visits within your local Matomo instance, you don't need a website with the JavaScript tracking code installed. Instead, you can generate sample visits using the VisitorGenerator plugin.

Start by creating a site in the Matomo Dashboard. Then, run the following commands on your host machine:

```
ddev matomo:console development:enable
ddev matomo:console plugin:activate VisitorGenerator
ddev matomo:console visitorgenerator:generate-visits
```

## Known issues

Currently, the screenshots generated by UI tests do not match those generated in the CI environment. However, this does not affect the functionality of the tests themselves. The screenshots should still be good enough to verify that your changes or plugin are working correctly.

---

### Overview (`js/README.md`)

## Introduction

The js/ folder contains:

* index.php - a servlet described below
* piwik.js  - the uncompressed piwik.js source for you to study or reference
* README.md - this documentation file

### Why Use "js/index.php"?

* js/index.php (or implicitly as "js/") can be used to serve up the minified
  piwik.js

    * it supports conditional-GET and Last-Modified, so piwik.js can be cached
      by the browser
    * it supports deflate/gzip compression if your web server (e.g., Apache
      without mod_deflate or mod_gzip), shrinking the data transfer to 8K

* js/index.php (or implicitly as "js/") can also act as a proxy to matomo.php

* If you are concerned about the impact of browser-based privacy filters which
  attempt to block tracking, you can change your tracking code to use "js/"
  instead of "piwik.js" and "matomo.php", respectively.

  Note that in order for [Page Overlay](https://matomo.org/docs/page-overlay/) to work, the Piwik tracker method `setAPIUrl()` needs to be called with its parameter pointing to the root directory of Piwik. E.g.:

  ```js
  _paq.push(['setAPIUrl', u]);

  ```

## Deployment

To minify core JavaScript you may run the following command (with Development Mode enabled):

  ```bash
  $ ./console development:build-tracker-js
  ```

(Note: this command can also be used to minify plugin tracker JavaScript.)

To manually minify the file:

* piwik.js is minified using YUICompressor 2.4.8.
  To install YUICompressor run:
 
  ```bash
  $ cd /path/to/piwik/js/
  $ wget https://github.com/yui/yuicompressor/releases/download/v2.4.8/yuicompressor-2.4.8.zip
  $ unzip yuicompressor-2.4.8.zip
  ```

  To compress the code containing the evil "eval", run:

  ```bash
  $ cd /path/to/piwik/js/
  $ sed '/<DEBUG>/,/<\/DEBUG>/d' < piwik.js | sed 's/eval/replacedEvilString/' | java -jar yuicompressor-2.4.8.jar --type js --line-break 1000 | sed 's/replacedEvilString/eval/' | sed 's/^[/][*]/\/*!/' > piwik.min.js && cp piwik.min.js ../piwik.js && cp piwik.min.js ../matomo.js
  ```

  This will generate the minify /path/to/piwik/js/piwik.min.js and copy it to
  /path/to/piwik/piwik.js. Both "js/piwik.min.js" and "piwik.js" need to be committed.
  
  We recommend to execute this command under Linux. It has not been tested with Windows and 
  MacOS might add a trailing newline which fails tests.
VisitorGeolocatorTest
* In a production environment, the tests/javascript folder is not used and can
  be removed (if present).

* We use /*! to include Piwik's license header in the minified source. Read
  Stallman's "The JavaScript Trap" for more information.

* Information about the current version number you have installed can be found under [What version of Piwik do I have?](https://matomo.org/faq/how-to-update/faq_8/).

---

### Overview (`lang/README.md`)

## Contribute

If you want to improve an existing Matomo translation or contribute a new translation, please have a look at our [Weblate project](https://hosted.weblate.org/engage/matomo/).

We likely won't accept pull requests for translations on GitHub since we manage all translations in this separate system. Translations will automatically be transferred to GitHub from time to time!

If you have any questions feel free to contact the team at translations@matomo.org.

---

### Overview (`libs/README.md`)

## Legal notice

See the [LEGALNOTICE file](https://github.com/matomo-org/matomo/blob/6.x-dev/LEGALNOTICE).

## Matomo modifications to libs/

In general, bug fixes and improvements are reported upstream.  Until these are
included upstream, we maintain a list of bug fixes and local mods made to
third-party libraries:

 * HTML/Quickform2/
   - in r2626, php 5.1.6 incompatibility
   - in r3040, exception classes don't follow PEAR naming convention
 * Zend/
   - strip require_once (to support autoloading)
   - fix #6980 ("Array to string conversion") in `Zend/Session/Exception.php`
   - various adjustments to remove unused parts of Zend Framework
   - Make sure sessions work when storing notifications
 * materialize/
   - in materialize.min.css removed the loading of Roboto fonts as the paths do not match and couldn't be loaded. Also
     we do not want to load as many different font styles of Roboto font.

---

### Overview (`libs/Authenticator/README.md`)

Google Authenticator PHP class
==============================

* Copyright (c) 2012-2016, [http://www.phpgangsta.de](http://www.phpgangsta.de)
* Author: Michael Kliewe, [@PHPGangsta](http://twitter.com/PHPGangsta) and [contributors](https://github.com/PHPGangsta/GoogleAuthenticator/graphs/contributors)
* Licensed under the BSD License.

[](https://travis-ci.org/PHPGangsta/GoogleAuthenticator)

This PHP class can be used to interact with the Google Authenticator mobile app for 2-factor-authentication. This class
can generate secrets, generate codes, validate codes and present a QR-Code for scanning the secret. It implements TOTP
according to [RFC6238](https://tools.ietf.org/html/rfc6238)

For a secure installation you have to make sure that used codes cannot be reused (replay-attack). You also need to
limit the number of verifications, to fight against brute-force attacks. For example you could limit the amount of
verifications to 10 tries within 10 minutes for one IP address (or IPv6 block). It depends on your environment.

Usage:
------

See following example:

```php
<?php
require_once 'PHPGangsta/GoogleAuthenticator.php';

$ga = new PHPGangsta_GoogleAuthenticator();
$secret = $ga->createSecret();
echo "Secret is: ".$secret."\n\n";

$qrCodeUrl = $ga->getQRCodeGoogleUrl('Blog', $secret);
echo "Google Charts URL for the QR-Code: ".$qrCodeUrl."\n\n";

$oneCode = $ga->getCode($secret);
echo "Checking Code '$oneCode' and Secret '$secret':\n";

$checkResult = $ga->verifyCode($secret, $oneCode, 2);    // 2 = 2*30sec clock tolerance
if ($checkResult) {
    echo 'OK';
} else {
    echo 'FAILED';
}
```
Running the script provides the following output:
```
Secret is: OQB6ZZGYHCPSX4AK

Google Charts URL for the QR-Code: https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/infoATphpgangsta.de%3Fsecret%3DOQB6ZZGYHCPSX4AK

Checking Code '848634' and Secret 'OQB6ZZGYHCPSX4AK':
OK
```

Installation:
-------------

- Use [Composer](https://getcomposer.org/doc/01-basic-usage.md) to
  install the package

- From project root directory execute following

```composer install```

- [Composer](https://getcomposer.org/doc/01-basic-usage.md) will take care of autoloading
  the library. Just include the following at the top of your file

  `require_once __DIR__ . '/../vendor/autoload.php';`

Run Tests:
----------

- All tests are inside `tests` folder.
- Execute `composer install` and then run the tests from project root
  directory
- Run as `phpunit tests` from the project root directory


ToDo:
-----
- ??? What do you need?

Notes:
------

If you like this script or have some features to add: contact me, visit my blog, fork this project, send pull requests, you know how it works.

---

### Overview (`misc/package/README.md`)

# Matomo Packages

We build the main Matomo packages and Debian package using https://github.com/matomo-org/matomo-package

---

### Overview (`misc/proxy-hide-piwik-url/README.md`)

# Matomo Proxy Hide URL

The proxy script has been moved to [matomo/tracker-proxy](https://github.com/matomo-org/tracker-proxy).

---

### Overview (`node_modules/chroma-js/readme.md`)

# Chroma.js

Chroma.js is a tiny JavaScript library (8.5kB) for all kinds of color conversions and color scales.

### Usage


Initiate and manipulate colors:

```javascript
chroma('#D4F880').darken().hex();  // #9BC04B
```

Working with color scales is easy, too:

```javascript    
scale = chroma.scale(['white', 'red']);
scale(0.5).hex(); // #FF7F7F
```

Lab/Lch interpolation looks better than than RGB

```javascript    
chroma.scale(['white', 'red']).mode('lab');
```

Custom domains! Quantiles! Color Brewer!! 

```javascript    
chroma.scale('RdYlBu').domain(myValues, 7, 'quantiles');    
```

And why not use logarithmic color scales once in your life?

```javascript
chroma.scale(['lightyellow', 'navy']).domain([1, 100000], 7, 'log');    
```

### Like it?

Why not dive into the [API docs](https://github.com/gka/chroma.js/blob/master/doc/api.md) (quite short actually), and download [chroma.min.js](https://raw.github.com/gka/chroma.js/master/chroma.min.js) right away.

You can use it in node.js, too!

    npm install chroma-js


### Build instructions

To compile the coffee-script source files you have to run

    make clean
    make

To run the tests simply run

    vows test/*.coffee


### Similar Libraries / Prior Art

* [Chromatist](https://github.com/jrus/chromatist)
* [GrapeFruit](https://github.com/xav/Grapefruit) (Python)
* [colors.py](https://github.com/mattrobenolt/colors.py) (Python)
* [d3.js](https://github.com/mbostock/d3)


### Author

Chroma.js is written by [Gregor Aisch](http://driven-by-data.net).

### License

Released under [BSD license](http://opensource.org/licenses/BSD-3-Clause).
Versions prior to 0.4 were released under [GPL](http://www.gnu.org/licenses/gpl-3.0).

### Known issues

* HSI color conversion is experimental and produces weird results sometimes

### Further reading

* [How To Avoid Equidistant HSV Colors](https://vis4.net/blog/posts/avoid-equidistant-hsv-colors/)
* [Mastering Multi-hued Color Scales with Chroma.js](https://vis4.net/blog/posts/mastering-multi-hued-color-scales/)

---

### Overview (`node_modules/iframe-resizer/README.md`)

<img src="https://raw.githubusercontent.com/davidjbradshaw/iframe-resizer/master/img/logo-no-background.svg" alt="iFrame Resizer" style="margin-bottom: -20">

[](http://badge.fury.io/js/iframe-resizer)
[](https://npm-stat.com/charts.html?package=iframe-resizer&from=2014-12-31)
[](https://www.jsdelivr.com/package/npm/iframe-resizer)
[](https://coveralls.io/github/davidjbradshaw/iframe-resizer)

This library enables the automatic resizing of the height and width of both same and cross domain iFrames to fit their contained content. It provides a range of features to address the most common issues with using iFrames, these include:

- Height and width resizing of the iFrame to content size.
- Works with multiple and nested iFrames.
- Domain authentication for cross domain iFrames.
- Provides a range of page size calculation methods to support complex CSS layouts.
- Detects changes to the DOM that can cause the page to resize using [MutationObserver](https://developer.mozilla.org/en/docs/Web/API/MutationObserver).
- Detects events that can cause the page to resize (Window Resize, CSS Animation and Transition, Orientation Change and Mouse events).
- Simplified messaging between iFrame and host page via [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/window.postMessage).
- Fixes in page links in iFrame and supports links between the iFrame and parent page.
- Provides custom sizing and scrolling methods.
- Exposes parent position and viewport size to the iFrame.
- Provides `onMouseEnter` and `onMouseLeave` events for the iFrame.
- Works with [ViewerJS](http://viewerjs.org/) to support PDF and ODF documents.

## Donate

Iframe-resizer is the result of many 100s of hours of work, if you would like to join others in showing support for the continued development of this project, then please feel free to buy me a coffee.

<a href="https://www.buymeacoffee.com/davidjbradshaw " target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>

## Getting Started

### Install

This package can be installed via NPM (`npm install iframe-resizer --save`).

### Usage

The package contains two minified JavaScript files in the [js](https://github.com/davidjbradshaw/iframe-resizer/tree/master/js) folder. The first ([iframeResizer.min.js](https://raw.githubusercontent.com/davidjbradshaw/iframe-resizer/master/js/iframeResizer.min.js)) is for the page hosting the iFrames. It can be called with via JavaScript:

```js
const iframes = iFrameResize( [{options}], [css selector] || [iframe] );
```

The second file ([iframeResizer.contentWindow.min.js](https://raw.github.com/davidjbradshaw/iframe-resizer/master/js/iframeResizer.contentWindow.min.js)) needs placing in the page(s) contained within your iFrame. <i>This file is designed to be a guest on someone else's system, so has no dependencies and won't do anything until it's activated by a message from the containing page</i>.

### Typical setup

The normal configuration is to have the iFrame resize when the browser window changes size or the content of the iFrame changes. To set this up you need to configure one of the dimensions of the iFrame to a percentage and tell the library to only update the other dimension. Normally you would set the width to 100% and have the height scale to fit the content.

```html
<style>
  iframe {
    width: 1px;
    min-width: 100%;
  }
</style>
<script src="/js/iframeResizer.min.js"></script>
<iframe id="myIframe" src="http://anotherdomain.com/iframe.html"></iframe>
<script>
  iFrameResize({ log: true }, '#myIframe')
</script>
```

**Note:** Using _min-width_ to set the width of the iFrame, works around an issue in iOS that can prevent the iFrame from sizing correctly.

If you have problems, check the [troubleshooting](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/troubleshooting.md) section.

### Example

To see this working take a look at this [example](https://davidjbradshaw.github.io/iframe-resizer/example/) and watch the [console](https://developer.mozilla.org/en-US/docs/Tools/Web_Console).

## API Documentation

IFrame-Resizer provides an extensive range of options and APIs for both the parent page and the iframed page.

- **Parent Page API**
  - [Options](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/parent_page/options.md)
  - [Events](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/parent_page/events.md)
  - [Methods](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/parent_page/methods.md)
- **IFramed Page API**
  - [Options](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/iframed_page/options.md)
  - [Events](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/iframed_page/events.md)
  - [Methods](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/iframed_page/methods.md)
- **Use with Libraries and Frameworks**
  - [React](https://github.com/davidjbradshaw/iframe-resizer-react)
  - [Vue](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/use_with/vue.md)
  - [Nuxt](https://github.com/davidjbradshaw/iframe-resizer/issues/831#issuecomment-665760332)
  - [Angular](https://github.com/davidjbradshaw/iframe-resizer/issues/478#issuecomment-347958630)
  - [Ember](https://github.com/alexlafroscia/ember-iframe-resizer-modifier)
  - [jQuery](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/use_with/jquery.md)
  - [Google Apps Script](https://stackoverflow.com/a/65724113/2087070)
- [Troubleshooting](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/troubleshooting.md)
- [Upgrade from version 3](https://github.com/davidjbradshaw/iframe-resizer/blob/master/docs/upgrade.md)
- [Version history](https://github.com/davidjbradshaw/iframe-resizer/blob/master/CHANGELOG.md)

## License

Copyright &copy; 2013-23 [David J. Bradshaw](https://github.com/davidjbradshaw) -
Licensed under the [MIT License](LICENSE)

---

### Overview (`node_modules/jquery-mousewheel/README.md`)

# jQuery Mouse Wheel Plugin

A [jQuery](https://jquery.com/) plugin that adds cross-browser mouse wheel support with delta normalization.

In order to use the plugin, simply bind the `mousewheel` event to an element.

It also provides two deprecated helper methods called `mousewheel` and `unmousewheel`
that act just like other event helper methods in jQuery.

The event object is updated with the normalized `deltaX` and `deltaY` properties.
In addition, there is a new property on the event object called `deltaFactor`. Multiply
the `deltaFactor` by `deltaX` or `deltaY` to get the scroll distance that the browser
has reported.

Here is an example of using both the bind and helper method syntax:

```js
// using on
$( "#my_elem" ).on( "mousewheel", function( event ) {
    console.log( event.deltaX, event.deltaY, event.deltaFactor );
} );

// using the event helper - not recommended!
$( "#my_elem" ).mousewheel( function( event ) {
    console.log( event.deltaX, event.deltaY, event.deltaFactor );
} );
```

The old behavior of adding three arguments (`delta`, `deltaX`, and `deltaY`) to the
event handler is now deprecated and will be removed in later releases.


## The Deltas...

The combination of browsers, operating systems, and devices offer a huge range of possible delta values. In fact if the user
uses a trackpad and then a physical mouse wheel the delta values can differ wildly. This plugin normalizes those
values so you get a whole number starting at +-1 and going up in increments of +-1 according to the force or
acceleration that is used. This number has the potential to be in the thousands depending on the device.

### Getting the scroll distance

In some use-cases we prefer to have the normalized delta but in others we want to know how far the browser should
scroll based on the users input. This can be done by multiplying the `deltaFactor` by the `deltaX` or `deltaY`
event property to find the scroll distance the browser reported.

The `deltaFactor` property was added to the event object in 3.1.5 so that the actual reported delta value can be
extracted. This is a non-standard property.

## Building the code in the repo & running tests

```sh
git clone git@github.com:jquery/jquery-mousewheel.git
cd jquery-mousewheel/
npm install
npm test
```

The unit tests are _very_ basic sanity checks; improvements welcome.
To fully test the plugin, load [test/index.html](test/index.html) in each supported
browser and follow the instructions at the top of the file after the unit tests finish.

---

### Overview (`node_modules/jquery.scrollto/README.md`)

# jQuery.scrollTo

Lightweight, cross-browser and highly customizable animated scrolling with jQuery

[](http://flattr.com/thing/2081384/fleslerjquery-scrollTo-on-GitHub)
[](http://badge.fury.io/gh/flesler%2Fjquery.scrollTo)

## Installation
The plugin requires jQuery 1.8 or higher.

Via [bower](https://github.com/flesler/jquery.scrollTo/blob/master/bower.json):
```bash
bower install jquery.scrollTo
```
Via [npm](https://www.npmjs.com/package/jquery.scrollto):
```bash
npm install jquery.scrollto
```
Via [packagist](https://packagist.org/packages/flesler/jquery.scrollTo):
```php
php composer.phar require --prefer-dist flesler/jquery.scrollto "*"
```

### Using a public CDN

CDN provided by [jsdelivr](http://www.jsdelivr.com/#!jquery.scrollto)
```html
<script src="//cdn.jsdelivr.net/npm/jquery.scrollto@2.1.3/jquery.scrollTo.min.js"></script>
```
CDN provided by [cdnjs](https://cdnjs.com/libraries/jquery-scrollTo)
```html
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-scrollTo/2.1.3/jquery.scrollTo.min.js"></script>
```

### Downloading Manually

If you want the latest stable version, get the latest release from the [releases page](https://github.com/flesler/jquery.scrollTo/releases).

## 2.0

Version 2.0 has been recently released. It is mostly backwards compatible, if you have any issue first check [this link](https://github.com/flesler/jquery.scrollTo/wiki/Migrating-to-2.0).
If your problem is not solved then go ahead and [report the issue](https://github.com/flesler/jquery.scrollTo/issues/new).

## Usage

jQuery.scrollTo's signature is designed to resemble [$().animate()](http://api.jquery.com/animate/).

```js
$(element).scrollTo(target[,duration][,settings]);
```

### _element_

This must be a scrollable element, to scroll the whole window use `$(window)`.

### _target_

This defines the position to where `element` must be scrolled. The plugin supports all these formats:
 * A number with a fixed position: `250`
 * A string with a fixed position with px: `"250px"`
 * A string with a percentage (of container's size): `"50%"`
 * A string with a relative step: `"+=50px"`
 * An object with `left` and `top` containining any of the aforementioned: `{left:250, top:"50px"}`
 * The string `"max"` to scroll to the end.
 * A string selector that will be relative to the element to scroll: `".section:eq(2)"`
 * A DOM element, probably a child of the element to scroll: `document.getElementById("top")`
 * A jQuery object with a DOM element: `$("#top")`

### _settings_

The `duration` parameter is a shortcut to the setting with the same name.
These are the supported settings:
 * __axis__: The axes to animate: `xy` (default), `x`, `y`, `yx`
 * __interrupt__: If `true` will cancel the animation if the user scrolls. Default is `false`
 * __limit__: If `true` the plugin will not scroll beyond the container's size. Default is `true`
 * __margin__: If `true`, subtracts the margin and border of the `target` element. Default is `false`
 * __offset__: Added to the final position, can be a number or an object with `left` and `top`
 * __over__: Adds a % of the `target` dimensions: `{left:0.5, top:0.5}`
 * __queue__: If `true` will scroll one `axis` and then the other. Default is `false`
 * __onAfter(target, settings)__: A callback triggered when the animation ends (jQuery's `complete()`)
 * __onAfterFirst(target, settings)__: A callback triggered after the first axis scrolls when queueing

You can add any setting supported by [$().animate()](http://api.jquery.com/animate/#animate-properties-options) as well:

 * __duration__: Duration of the animation, default is `0` which makes it instantaneous
 * __easing__: Name of an easing equation, you must register the easing function: `swing`
 * __fail()__: A callback triggered when the animation is stopped (f.e via `interrupt`)
 * __step()__: A callback triggered for every animated property on every frame
 * __progress()__: A callback triggered on every frame
 * And more, check jQuery's [documentation](http://api.jquery.com/animate/#animate-properties-options)

### window shorthand

You can use `$.scrollTo(...)` as a shorthand for `$(window).scrollTo(...)`.

### Changing the default settings

As with most plugins, the default settings are exposed so they can be changed.
```js
$.extend($.scrollTo.defaults, {
  axis: 'y',
  duration: 800
});
```

### Stopping the animation

jQuery.scrollTo ends up creating ordinary animations which can be stopped by calling [$().stop()](http://api.jquery.com/stop/) or [$().finish()](http://api.jquery.com/finish/) on the same element you called `$().scrollTo()`, including the `window`.
Remember you can pass a `fail()` callback to be called when the animation is stopped.

### onAfter and requestAnimationFrame

jQuery.scrollTo has a `onAfter` callback for work that runs after the animation finishes. It will be called before the `scroll` event fires. To combat this you can use  [requestAnimationFrame](http://caniuse.com/#feat=requestanimationframe) to do work on the next tick. It is available in many browsers, but you may want to [polyfill](https://github.com/chrisdickinson/raf) for the few it does not support.
```js
$.scrollTo(100, {
  onAfter: function() {
    requestAnimationFrame(function() {
        $(".result").addClass("selected");
    });
  }
});
```

## Demo

Check the [demo](http://demos.flesler.com/jquery/scrollTo/) to see every option in action.

## Complementary plugins

There are two plugins, also created by me that depend on jQuery.scrollTo and aim to simplify certain use cases.

### [jQuery.localScroll](https://github.com/flesler/jquery.localScroll)

This plugin makes it very easy to implement anchor navigation.
If you don't want to include another plugin, you can try using something like [this minimalistic gist](https://gist.github.com/flesler/3f3e1166690108abf747).

### [jQuery.serialScroll](https://github.com/flesler/jquery.serialScroll)

This plugin simplifies the creation of scrolling slideshows.

## License

(The MIT License)

Copyright (c) 2007 Ariel Flesler <aflesler@gmail.com>

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.

---

### Overview (`node_modules/jquery/README.md`)

# jQuery

> jQuery is a fast, small, and feature-rich JavaScript library.

For information on how to get started and how to use jQuery, please see [jQuery's documentation](https://api.jquery.com/).
For source files and issues, please visit the [jQuery repo](https://github.com/jquery/jquery).

If upgrading, please see the [blog post for 3.7.1](https://blog.jquery.com/2023/08/28/jquery-3-7-1-released-reliable-table-row-dimensions/). This includes notable differences from the previous version and a more readable changelog.

## Including jQuery

Below are some of the most common ways to include jQuery.

### Browser

#### Script tag

```html
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
```

#### Webpack / Browserify / Babel

There are several ways to use [Webpack](https://webpack.js.org/), [Browserify](http://browserify.org/) or [Babel](https://babeljs.io/). For more information on using these tools, please refer to the corresponding project's documentation. In the script, including jQuery will usually look like this:

```js
import $ from "jquery";
```

If you need to use jQuery in a file that's not an ECMAScript module, you can use the CommonJS syntax:

```js
var $ = require( "jquery" );
```

#### AMD (Asynchronous Module Definition)

AMD is a module format built for the browser. For more information, we recommend [require.js' documentation](https://requirejs.org/docs/whyamd.html).

```js
define( [ "jquery" ], function( $ ) {

} );
```

### Node

To include jQuery in [Node](https://nodejs.org/), first install with npm.

```sh
npm install jquery
```

For jQuery to work in Node, a window with a document is required. Since no such window exists natively in Node, one can be mocked by tools such as [jsdom](https://github.com/jsdom/jsdom). This can be useful for testing purposes.

```js
const { JSDOM } = require( "jsdom" );
const { window } = new JSDOM( "" );
const $ = require( "jquery" )( window );
```

---

### Overview (`node_modules/mousetrap/README.md`)

# Mousetrap
[](https://cdnjs.com/libraries/mousetrap)

Mousetrap is a simple library for handling keyboard shortcuts in Javascript.

It is licensed under the Apache 2.0 license.

It is around **2kb** minified and gzipped and **4.5kb** minified, has no external dependencies, and has been tested in the following browsers:

- Internet Explorer 6+
- Safari
- Firefox
- Chrome

It has support for `keypress`, `keydown`, and `keyup` events on specific keys, keyboard combinations, or key sequences.

## Getting started

1. Include mousetrap on your page before the closing `</body>` tag

    ```html
    <script src="/path/to/mousetrap.min.js"></script>
    ```

    or install `mousetrap` from `npm` and require it

    ```js
    var Mousetrap = require('mousetrap');
    ```

2. Add some keyboard events to listen for

    ```html
    <script>
        // single keys
        Mousetrap.bind('4', function() { console.log('4'); });
        Mousetrap.bind("?", function() { console.log('show shortcuts!'); });
        Mousetrap.bind('esc', function() { console.log('escape'); }, 'keyup');

        // combinations
        Mousetrap.bind('command+shift+k', function() { console.log('command shift k'); });

        // map multiple combinations to the same callback
        Mousetrap.bind(['command+k', 'ctrl+k'], function() {
            console.log('command k or control k');

            // return false to prevent default browser behavior
            // and stop event from bubbling
            return false;
        });

        // gmail style sequences
        Mousetrap.bind('g i', function() { console.log('go to inbox'); });
        Mousetrap.bind('* a', function() { console.log('select all'); });

        // konami code!
        Mousetrap.bind('up up down down left right left right b a enter', function() {
            console.log('konami code');
        });
    </script>
    ```

## Why Mousetrap?

There are a number of other similar libraries out there so what makes this one different?

- There are no external dependencies, no framework is required
- You are not limited to `keydown` events (You can specify `keypress`, `keydown`, or `keyup` or let Mousetrap choose for you).
- You can bind key events directly to special keys such as `?` or `*` without having to specify `shift+/` or `shift+8` which are not consistent across all keyboards
- It works with international keyboard layouts
- You can bind Gmail like key sequences in addition to regular keys and key combinations
- You can programatically trigger key events with the `trigger()` method
- It works with the numeric keypad on your keyboard
- The code is well documented/commented

## Tests

Unit tests are run with <a href="https://mochajs.org/">mocha</a>.

### Running in browser

[View it online](http://rawgit.com/ccampbell/mousetrap/master/tests/mousetrap.html) to check your browser compatibility. You may also download the repo and open `tests/mousetrap.html` in your browser.

### Running with Node.js

1. Install development dependencies

    ```sh
    cd /path/to/repo
    npm install
    ```

3. Run tests

    ```sh
    npm test
    ```

## Documentation

Full documentation can be found at https://craig.is/killing/mice

---

### Overview (`node_modules/qrcodejs2/README.md`)

# QRCode.js
QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM.
QRCode.js has no dependencies.

## Basic Usages
```
<div id="qrcode"></div>
<script type="text/javascript">
new QRCode(document.getElementById("qrcode"), "http://jindo.dev.naver.com/collie");
</script>
```

or with some options

```
var qrcode = new QRCode("test", {
	text: "http://jindo.dev.naver.com/collie",
	width: 128,
	height: 128,
	colorDark : "#000000",
	colorLight : "#ffffff",
	correctLevel : QRCode.CorrectLevel.H
});
```

and you can use some methods

```
qrcode.clear(); // clear the code.
qrcode.makeCode("http://naver.com"); // make another code.
```

## Browser Compatibility
IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC.

## License
MIT License

## Contact
twitter @davidshimjs

[](https://bitdeli.com/free "Bitdeli Badge")

---

### Overview (`node_modules/sprintf-js/README.md`)

# sprintf-js

[![Build Status][travisci-image]][travisci-url] [![NPM Version][npm-image]][npm-url] [![Dependency Status][dependencies-image]][dependencies-url] [![devDependency Status][dev-dependencies-image]][dev-dependencies-url]

[travisci-image]: https://travis-ci.org/alexei/sprintf.js.svg?branch=master
[travisci-url]: https://travis-ci.org/alexei/sprintf.js

[npm-image]: https://badge.fury.io/js/sprintf-js.svg
[npm-url]: https://badge.fury.io/js/sprintf-js

[dependencies-image]: https://david-dm.org/alexei/sprintf.js.svg
[dependencies-url]: https://david-dm.org/alexei/sprintf.js

[dev-dependencies-image]: https://david-dm.org/alexei/sprintf.js/dev-status.svg
[dev-dependencies-url]: https://david-dm.org/alexei/sprintf.js#info=devDependencies

**sprintf-js** is a complete open source JavaScript `sprintf` implementation for the **browser** and **Node.js**.

**Note: as of v1.1.1 you might need some polyfills for older environments. See [Support](#support) section below.**

## Usage

    var sprintf = require('sprintf-js').sprintf,
        vsprintf = require('sprintf-js').vsprintf

    sprintf('%2$s %3$s a %1$s', 'cracker', 'Polly', 'wants')
    vsprintf('The first 4 letters of the english alphabet are: %s, %s, %s and %s', ['a', 'b', 'c', 'd'])

## Installation

### NPM

    npm install sprintf-js

### Bower

    bower install sprintf

## API

### `sprintf`

Returns a formatted string:

    string sprintf(string format, mixed arg1?, mixed arg2?, ...)

### `vsprintf`

Same as `sprintf` except it takes an array of arguments, rather than a variable number of arguments:

    string vsprintf(string format, array arguments?)

## Format specification

The placeholders in the format string are marked by `%` and are followed by one or more of these elements, in this order:

* An optional number followed by a `$` sign that selects which argument index to use for the value. If not specified, arguments will be placed in the same order as the placeholders in the input string.
* An optional `+` sign that forces to precede the result with a plus or minus sign on numeric values. By default, only the `-` sign is used on negative numbers.
* An optional padding specifier that says what character to use for padding (if specified). Possible values are `0` or any other character preceded by a `'` (single quote). The default is to pad with *spaces*.
* An optional `-` sign, that causes `sprintf` to left-align the result of this placeholder. The default is to right-align the result.
* An optional number, that says how many characters the result should have. If the value to be returned is shorter than this number, the result will be padded. When used with the `j` (JSON) type specifier, the padding length specifies the tab size used for indentation.
* An optional precision modifier, consisting of a `.` (dot) followed by a number, that says how many digits should be displayed for floating point numbers. When used with the `g` type specifier, it specifies the number of significant digits. When used on a string, it causes the result to be truncated.
* A type specifier that can be any of:
    * `%` — yields a literal `%` character
    * `b` — yields an integer as a binary number
    * `c` — yields an integer as the character with that ASCII value
    * `d` or `i` — yields an integer as a signed decimal number
    * `e` — yields a float using scientific notation
    * `u` — yields an integer as an unsigned decimal number
    * `f` — yields a float as is; see notes on precision above
    * `g` — yields a float as is; see notes on precision above
    * `o` — yields an integer as an octal number
    * `s` — yields a string as is
    * `t` — yields `true` or `false`
    * `T` — yields the type of the argument<sup><a href="#fn-1" name="fn-ref-1">1</a></sup>
    * `v` — yields the primitive value of the specified argument
    * `x` — yields an integer as a hexadecimal number (lower-case)
    * `X` — yields an integer as a hexadecimal number (upper-case)
    * `j` — yields a JavaScript object or array as a JSON encoded string

## Features

### Argument swapping

You can also swap the arguments. That is, the order of the placeholders doesn't have to match the order of the arguments. You can do that by simply indicating in the format string which arguments the placeholders refer to:

    sprintf('%2$s %3$s a %1$s', 'cracker', 'Polly', 'wants')

And, of course, you can repeat the placeholders without having to increase the number of arguments.

### Named arguments

Format strings may contain replacement fields rather than positional placeholders. Instead of referring to a certain argument, you can now refer to a certain key within an object. Replacement fields are surrounded by rounded parentheses - `(` and `)` - and begin with a keyword that refers to a key:

    var user = {
        name: 'Dolly',
    }
    sprintf('Hello %(name)s', user) // Hello Dolly

Keywords in replacement fields can be optionally followed by any number of keywords or indexes:

    var users = [
        {name: 'Dolly'},
        {name: 'Molly'},
        {name: 'Polly'},
    ]
    sprintf('Hello %(users[0].name)s, %(users[1].name)s and %(users[2].name)s', {users: users}) // Hello Dolly, Molly and Polly

Note: mixing positional and named placeholders is not (yet) supported

### Computed values

You can pass in a function as a dynamic value and it will be invoked (with no arguments) in order to compute the value on the fly.

    sprintf('Current date and time: %s', function() { return new Date().toString() })

### AngularJS

You can use `sprintf` and `vsprintf` (also aliased as `fmt` and `vfmt` respectively) in your AngularJS projects. See `demo/`.

## Support

### Node.js

`sprintf-js` runs in all active Node versions (4.x+).

### Browser

`sprintf-js` should work in all modern browsers. As of v1.1.1, you might need polyfills for the following:

 - `String.prototype.repeat()` (any IE)
 - `Array.isArray()` (IE < 9)
 - `Object.create()` (IE < 9)

YMMV

## License

**sprintf-js** is licensed under the terms of the BSD 3-Clause License.

## Notes

<small><sup><a href="#fn-ref-1" name="fn-1">1</a></sup> `sprintf` doesn't use the `typeof` operator. As such, the value `null` is a `null`, an array is an `array` (not an `object`), a date value is a `date` etc.</small>

---

### Overview (`node_modules/visibilityjs/README.md`)

# Visibility.js [](https://travis-ci.org/ai/visibilityjs)

<img align="right" width="100" src="./logo.svg" title="Visibility.js logo by Eugenia Tuchapets">

Visibility.js is a wrapper for the [Page Visibility API]. It hides vendor prefixes and adds high level functions.

Page Visibility API allows you to determine whether your web page is either visible to
a user or hidden in background tab or prerendering. It allows you to use
the page visibility state in JavaScript logic and improve browser performance
by disabling unnecessary timers and AJAX requests, or improve user interface
experience (for example, by stopping video playback or slideshow when user
switches to another browser tab).

Moreover, you can detect if the browser is just [prerendering] the page while
the user has still not opened the link, and don’t count this as a visit in your
analytics module, or do not run heavy calculations or other actions which will
disable the prerendering.

Page Visibility API is [natively supported] by all browsers. For old browsers
you can use `lib/visibility.fallback.js` with focus/blur hack (note that this
hack has an issue: when browser just lose focus but still visible for user, its state will change to [hidden]).

<a href="https://evilmartians.com/?utm_source=visibilityjs">
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
</a>

[Page Visibility API]: http://www.w3.org/TR/page-visibility/
[prerendering]:        http://code.google.com/chrome/whitepapers/prerender.html
[natively supported]:  http://caniuse.com/pagevisibility

## Translations

Документация на русском:
[habrahabr.ru/blogs/javascript/125833/](http://habrahabr.ru/blogs/javascript/125833/)

## States

Currently the Page Visibility API supports three visibility states:

* `visible`: user has opened the page and works within it.
* `hidden`: user has switched to another tab or minimized browser window.
* `prerender`: browser is just prerendering a page which may possibly be opened
   by the user to make the apparent loading time smaller.

## Timers

The main use case for this library is to enable some of the times only when
content is visible to the user, i.e. the ones animating a countdown animation.

`Visibility.every(interval, callback)` is similar to
`setInterval(callback, interval)`, but calls `callback` every `interval` ms only
if the page is visible. For example, let’s create a countdown timer:

```js
Visibility.every(1000, function () {
    updateCountdownAnimation();
});
```

You can provide an additional interval which will be used when the page
is hidden. In next example, a check for inbox updates will be run every 1 minute
for a visible page and every 5 minutes for a hidden one:

```js
var minute = 60 * 1000;
Visibility.every(minute, 5 * minute, function () {
    checkForEmail();
});
```

When the page becomes visible, if the callback has not been called in longer than
the visible interval, it will be called immediately. In the example above, if you
hid the page for 9 minutes, `checkForEmail` will get called once while the page is hidden,
and immediately when it is made visible.

`Visibility.every` returns a timer identifier, much like the `setInterval`
function. However, it cannot be passed to `clearInterval`, and you should use
`Visibility.stop(id)` to stop the timer.

```js
var slideshow = Visibility.every(5 * 1000, function () {
    nextSlide();
});

$('.stopSlideshow').click(function () {
    Visibility.stop(slideshow);
});
```

If the browser does not support the Page Visibility API, `Visibility.every` will
fall back to `setInterval`, and `callback` will be run every `interval` ms for
both the hidden and visible pages.

## Initializers

Another common use case is when you need to execute some actions upon a switch to
particular visibility state.

### Waiting until the page becomes visible

`Visibility.onVisible(callback)` checks current state of the page. If it is
visible now, it will run `callback`, otherwise it will wait until state changes
to `visible`, and then run `callback`.

For example, let’s show an animated notification only when the page is visible,
so if some user opens a page in the background, the animation will delay until
the page becomes visible, i.e. until the user has switched
to a tab with the page:

```js
Visibility.onVisible(function () {
    startIntroAnimation();
});
```

If a browser doesn’t support Page Visibility API, `Visibility.onVisible`
will run the `callback` immediately.

### Wait until the page is opened after prerendering

A web developer can hint a browser (using Prerendering API) that an user
is likely to click on some link (i.e. on a “Next” link in a multi-page article),
and the browser then may prefetch and prerender the page, so that the user will
not wait after actually going via the link.

But you may not want to count the browser prerendering a page as a visitor in
your analytics system. Moreover, the browser will disable prerendering if you
will try to do heavy computations or use audio/video tags on the page. So, you
may decide to not run parts of the code while prerendering and wait until the
user actually opens the link.

You can use `Visibility.afterPrerendering(callback)` in this cases. For example,
this code will only take real visitors (and not page prerenderings) into
account:

```js
Visibility.afterPrerendering(function () {
    Statistics.countVisitor();
});
```

If the browser doesn’t support Page Visibility API,
`Visibility.afterPrerendering` will run `callback` immediately.

## Low-level API

In some cases you may need more low-level methods. For example, you may want to
count the time user has viewed the page in foreground and time it has stayed in
background.

`Visibility.isSupported()` will return `true` if browser supports the
Page Visibility API:

```js
if( Visibility.isSupported() ) {
    Statistics.startTrackingVisibility();
}
```

`Visibility.state()` will return a string with visibility state. More states
can be added in the future, so for most cases a simpler `Visibility.hidden()`
method can be used. It will return `true` if the page is hidden by any reason.
For example, while prerendering, `Visibility.state()` will return `"prerender"`,
but `Visibility.hidden()` will return `true`.

This code will aid in collecting page visibility statistics:

```js
$(document).load(function () {

    if ( 'hidden' == Visibility.state() ) {
        Statistics.userOpenPageInBackgroundTab();
    }
    if ( 'prerender' == Visibility.state() ) {
        Statistics.pageIsPrerendering();
    }

});
```

And this example will only enable auto-playing when the page is opening as a
visible tab (not a background one):

```js
$(document).load(function () {

   if ( !Visibility.hidden() ) {
       VideoPlayer.play();
   }

});
```

Using `Visibility.change(callback)` you can listen to visibility state changing
events. The `callback` takes 2 arguments: an event object and a state name.

Let’s collect some statistics with this events approach:

```js
Visibility.change(function (e, state) {
    Statistics.visibilityChange(state);
});
```

Method `change` returns listener ID. You can use it to unbind listener by
`Visibility.unbind(id)`:

```js
var listener = Visibility.change(function (e, state) {
    if ( !Visibility.hidden() ) {
       VideoPlayer.pause();
    }
});

VideoPlayer.onFinish(function () {
    Visibility.unbind(listener);
});
```

Methods `onVisible` and `afterPrerendering` will also return listener ID,
if they wait visibility state changes. If they execute callback immediately,
they return `true` if Page Visibility API is supported and `false`
if they can’t detect visibility state.

```js
var listener = Visibility.onVisible(function () {
    notification.takeAttention();
});

notification.onOutOfDate(function () {
    if ( typeof(listener) == 'number' ) {
        Visibility.unbind(listener);
    }
});
```

## Packages

Visibility.js is shipped with 4 files:

* `visibility.core` – core module.
* `visibility.timers` – `every` and `stop` methods to set `setInterval` depend
  on visibility state.
* `visibility` – `visibility.core` and `visibility.timers` together.
* `visibility.fallback` – fallback for browser without Page Visibility API.
  It use document `focus`/`blur` events, so document become to be hidden,
  when browser just lose focus, but still visible for user.

## Installing

Available by [NPM](https://www.npmjs.com/package/visibilityjs):

```
npm install --save visibilityjs
```

## Contributing

1. To run tests you need node.js and npm. For example, in Ubuntu run:

   ```sh
   sudo apt-get install nodejs npm
   ```

2. Next install npm dependencies:

   ```sh
   npm install
   ```

3. Run all tests:

   ```sh
   npm test
   ```

4. Also you can see real usage example in integration test
   `test/integration.html`.

---

### Overview (`plugins/AIProviders/README.md`)

# AIProviders

Configure AI provider connections and default model settings used by Matomo AI features.

Built-in providers: Anthropic, OpenAI, Google, AWS Bedrock, and a generic custom provider for OpenAI-compatible endpoints.

## Configuration

Settings are managed from **Administration > System > AI Providers**.

The plugin stores the default provider, default capability level, and provider connection settings as Matomo system settings (not shown on the generic plugin settings page). API keys are only returned to the administration UI as masked state, not as secret values.

On a managed environment, the default provider is forced and locked via the `[AIProviders] defaultProvider` config setting. The AI Providers settings page and its admin menu entry are hidden entirely.

### Custom provider and local LLM servers

The generic custom provider talks to any **OpenAI-compatible** Chat Completions API. This includes hosted OpenAI-compatible services as well as local LLM servers such as Ollama, LM Studio, llama.cpp (`llama-server`), vLLM and LocalAI, which all expose the same `/v1/chat/completions` wire format.

Two things matter when configuring it:

- **API base URL.** Enter the URL up to and including the OpenAI-compatible API root. Most often, that is the `/v1` path. Matomo appends `/chat/completions` itself, so do not include it. Examples:
  - Ollama: `http://localhost:11434/v1`
  - LM Studio: `http://localhost:1234/v1`
  - vLLM / llama.cpp: `http://localhost:8000/v1`


- **API key.** Optional for the custom provider. Many local servers run without authentication, so the key may be left blank.

- **Model.** The "test connection" action probes `GET {base}/models` and populates the model picker from the result. Pick the model to use. The custom provider has **no built-in default model**.

### Managed credentials

Provider connection settings can also be supplied through namespaced DI values, the `[AIProviders]` config section, or environment variables instead of the administration UI. Per field, managed values win over the database value:

```php
AIProviders.openaiApiKey
```

```ini
[AIProviders]
openaiApiKey = "..."                ; or env MATOMO_AIPROVIDERS_OPENAI_API_KEY
custom-providerApiKey = "..."       ; or env MATOMO_AIPROVIDERS_CUSTOM_PROVIDER_API_KEY
custom-providerEndpointUrl = "..."  ; or env MATOMO_AIPROVIDERS_CUSTOM_PROVIDER_ENDPOINT_URL
```

`<provider>EndpointUrl` only applies to the providers that have an endpoint field at all — the custom provider's base URL and AWS Bedrock's region. The fixed hosted providers (OpenAI, Anthropic, Google) always talk to their own API, so a value supplied for them is ignored.

Credentials supplied this way never appear in the UI as secret values and cannot be edited or removed there.

A managed API key is bound to the endpoint it belongs to. For a provider whose endpoint field is a free-form URL (the custom provider), supply `<provider>EndpointUrl` together with `<provider>ApiKey`: such a key is only ever sent to the endpoint supplied alongside it, and an endpoint stored on the instance is ignored. Without a managed endpoint the provider has no destination and reports as not connected, so do not set `<provider>ApiKey` for a provider whose endpoint the instance should choose itself.

Saving or testing a different endpoint is then rejected with an error naming the config key to set instead. Only the endpoint is bound this way, not the key: a connection test may still submit an API key of its own, which is then the one sent to the managed endpoint. Providers that expand the field into a host they control, such as AWS Bedrock with its region, are unaffected by the *key* binding.

Supplying `<provider>EndpointUrl` on its own — without an API key — locks the endpoint field the same way, for the custom provider and for Bedrock's region alike: the supplied value is the one used, and saving or testing a different one is rejected rather than silently discarded, so a connection test can never report a pairing that a save would refuse to store. An endpoint the instance had configured before stays in the database untouched and takes effect again once the supplied value is removed.

In a multi-tenant setup the `config.ini.php` is scoped to each tenant, so the `[AIProviders]` section is the natural place to give each tenant its own provider credentials and forced default. Environment variables are process-wide and shared across tenants, so prefer the config file when the value must differ per tenant.

### Restricted providers and the provider selection allowlist

A managed environment can demote providers to *restricted* (non-selectable) in the `AIProviders.filterAIProviders` event via `AIProvidersList::setSelectable()`. Restricted providers are hidden from every admin surface and can never become the default, but stay registered for completions.

Plugins listed in the allowlist may target a specific provider and model per request even though the default provider is forced.

```ini
[AIProviders]
defaultProvider = "openai"
providerSelectionAllowlist[] = "ExamplePlugin"
```


## Usage from other plugins

To use a provider from another plugin, call the AIProvider service like so:

```php
use Piwik\Container\StaticContainer;
use Piwik\Plugins\AIProviders\AIRequest;
use Piwik\Plugins\AIProviders\AIProviderService;

$service = StaticContainer::get(AIProviderService::class);
$response = $service->complete(new AIRequest('why is the sky blue, answer in 7 words', 'YourPlugin'));
$text = $response->getText();
```

When a managed environment forces a provider from configuration, the service also ignores caller-provided provider and model overrides unless the calling plugin is on the `providerSelectionAllowlist` (see above), in which case its requested provider and model are honoured.

`AIProviderResponse` returns the generated text plus request metadata. `toArray()` returns:

```php
[
    'providerId' => 'openai',              // provider identifier used for the request
    'providerName' => 'OpenAI',            // human-readable provider name
    'model' => 'gpt-4.1-mini',             // model used
    'text' => 'Generated response text.',
    'inputTokens' => 42,                   // input/prompt tokens reported by the provider, or null
    'outputTokens' => 12,                  // output/completion tokens reported by the provider, or null
    'cacheReadTokens' => null,             // input tokens served from the prompt cache, not in inputTokens, or null
    'cacheWriteTokens' => null,            // input tokens written to the prompt cache, not in inputTokens, or null
    'reasoningLevel' => 'none',            // reasoning level used
    'webSearchUsed' => false,              // whether provider-side web search actually ran
    'webSearchRequestCount' => null,       // searches performed, or null when none ran / not reported
    'webSearchQueries' => [],              // queries the model issued, when the provider echoes them
    'webSearchCitations' => [],            // ['url' => …, 'title' => …, 'domain' => …] per source
    'executionTimeMs' => 1234,             // total request time in milliseconds, including retries, or null
    'stopReason' => 'stop',                // provider stop reason, if available, or null
    'cost' => null,                        // USD cost billed by the provider, when it reports one, or null
]
```

`stopReason` is the provider's own vocabulary, not a normalised one. Anthropic, AWS Bedrock and Google
report `end_turn` / `max_tokens` (Google's `STOP` / `MAX_TOKENS` are mapped onto those, and `SAFETY` /
`RECITATION` onto `guardrail_intervened`); OpenAI reports `stop` / `length` on both its grounded and
ungrounded paths. Match against the values of the provider you resolved to, and pass unrecognised ones
through rather than treating them as failures.

### Web search (grounding)

Ask the provider to search the web before answering, and read the sources it used:

```php
$response = $service->complete(
    (new AIRequest($prompt, 'YourPlugin'))->withWebSearchEnabled(true)
);

if ($response->wasWebSearchUsed()) {
    foreach ($response->getWebSearchCitations() as $citation) {
        // $citation['url'], $citation['title'], $citation['domain']
    }
}
```

Supported by Anthropic, Google and OpenAI. Providers without a web search tool (AWS Bedrock, custom
provider) reject the request with an `AIProviderClientException` rather than silently answering
ungrounded. Call `$service->canUseWebSearch('YourPlugin')` first if you need to fall back: it answers
for the provider `complete()` would actually resolve to, which on a managed instance is the forced
provider rather than the one you asked for.

**The model decides whether to search.** All three providers let the model choose, and AIProviders does
not override that. A grounded request can come back with no search at all, so read
`wasWebSearchUsed()` and `getWebSearchRequestCount()` on the response for what actually happened.

Grounded answers arrive in fragments, which AIProviders reassembles: adjacent fragments are one
sentence split at a citation boundary and are joined as-is, while a fragment after a tool block or a
separate output message starts a new sentence and gets a single space. Never a newline, so a boundary
falling inside a JSON string value cannot break `getJsonData()`.

**Citation titles and queries are untrusted model output.** They are length-capped but otherwise
verbatim, so escape them where you render them. URLs are guaranteed to be `http(s)`.

**Grounding is not a marginal cost.** Every provider charges per search, and the retrieved page content
is billed as input tokens on top, so a grounded request costs a multiple of the same request
ungrounded rather than a little more. The number of searches is capped at
`AIRequest::DEFAULT_MAX_WEB_SEARCHES` (2) on Anthropic (`max_uses`) and OpenAI (`max_tool_calls`);
raise or lower it per request with `withMaxWebSearches()`. OpenAI also caps the retrieved context
(`search_context_size`: medium). Google exposes no cap at all and ignores the setting.

Grounded requests are slow (30-90s). The provider timeout defaults to 120s for them (30s otherwise).
That outlasts the default read timeout of every common web server and proxy in front of PHP (nginx
`fastcgi_read_timeout` and Apache `Timeout` are both 60s), which cut the connection whatever PHP is
configured to allow. So **grounded completions are meant for CLI commands and scheduled tasks**, which
have nothing in front of them. From a web request, either raise those limits yourself or lower the
timeout with `withTimeoutSeconds()`, or the connection dies before the provider answers. (PHP's own
`max_execution_time` is the lesser worry: on non-Windows SAPIs it does not advance while a cURL
transfer is waiting.) A retryable HTTP status also re-runs the searches, so it multiplies both the wall
time and the per-search fees.

Four provider-specific caveats:

- **Google's citation URLs are not publisher URLs.** It returns a grounding redirect, so `url` is a
  `vertexaisearch.cloud.google.com` link and only `domain`, taken from the chunk title which Google
  sets to the publisher host, identifies the publisher. Two sources from the same page therefore do
  not deduplicate for Google: count distinct `domain`, not distinct `url`. When the title is not a
  hostname, `domain` is an empty string rather than a guess.
- **On OpenAI, combining web search with JSON mode is degraded, not native.** OpenAI rejects
  `text.format` together with web search, so `withJsonResponse()` on a grounded request relies on the
  prompt instruction alone; handle a `null` from `getJsonData()`. The search itself still runs.
- **On Google, web search and JSON mode cannot combine at all, so the request is rejected** with an
  `AIProviderClientException`. Gemini would accept it and then answer without searching. Verified
  against `gemini-3.1-flash-lite`: asking for JSON suppresses grounding two independent ways, through
  `responseMimeType: application/json` and through the JSON instruction `withJsonResponse()` adds to
  the prompt, so dropping either one does not restore it. Drop one of the two calls; `canUseWebSearch()`
  cannot warn you, because it does not see the request.
- **Anthropic can pause mid-search.** With several searches to run it may end the turn early and expect
  the message back to resume. AIProviders is deliberately single-round-trip, so the partial answer is
  returned as-is with `getStopReason()` reporting `pause_turn`. Check it before treating a grounded
  Anthropic answer as complete.

### JSON mode

For structured output, call `withJsonResponse()` and read the decoded object with `getJsonData()`:

```php
$response = $service->complete(
    (new AIRequest($prompt, 'Goals'))->withJsonResponse()
);
$data = $response->getJsonData(); // array, or null if the model did not return valid JSON
```

Each provider asks for JSON the best way it can: `response_format` (OpenAI-compatible), `responseMimeType` (Google). And AIProviders always adds a system instruction to return a single JSON object, so it works for providers without a native option (for example Anthropic) too. The model can still occasionally return invalid JSON, so always handle a `null` from `getJsonData()`.

## Conversations (multi-turn, tool calling)

`complete()` is prompt-in/text-out. When your plugin maintains a back-and-forth conversation and dispatches tool calls itself (as AskMatomo does), use `AIConversationRequest` with `AIProviderService::converse()` instead. The service resolves the provider exactly like `complete()` (forced provider, allowlisted caller selection, then the configured default) and returns one assistant turn per call.

```php
use Piwik\Container\StaticContainer;
use Piwik\Plugins\AIProviders\AIConversationRequest;
use Piwik\Plugins\AIProviders\AIConversationResponse;
use Piwik\Plugins\AIProviders\AIProviderService;

$service = StaticContainer::get(AIProviderService::class);

$messages = [
    ['role' => 'user', 'content' => [['type' => 'text', 'text' => 'How many visits yesterday?']]],
];

$response = $service->converse(
    (new AIConversationRequest($messages, 'YourPlugin'))
        ->withSystemPrompt($systemPrompt)
        ->withTools($toolCatalog) // optional; MCP-aligned shape, see below
        ->withMaxTokens(2048)
);

if ($response->getStopReason() === AIConversationResponse::STOP_TOOL_USE) {
    // Run the requested tool_use blocks, append this turn and the tool
    // results to $messages, and call converse() again.
}
```

Messages, tools, and the assistant's content all use one **provider-agnostic canonical shape** documented in [`CanonicalMessage.php`](CanonicalMessage.php). Each provider translates that shape to and from its own wire format inside `converse()`. Running tools and appending their results to the history for the next call is the caller's responsibility. The service performs a single round-trip per call.

`AIConversationResponse` exposes the assistant content blocks (`getContent()`), the stop reason mapped onto the `STOP_*` constants (`getStopReason()`), a text convenience (`getText()`), token usage, and the raw decoded provider response. Treat an unknown stop reason like `STOP_END_TURN`. Unlike `complete()`, an empty text turn is valid here: a turn may consist solely of `tool_use` blocks.

Not every provider supports conversations. Gate conversational features on availability.

```php
if (!$service->canConverse()) {
    // Hide or disable the feature.
}
```

## Usage limits and metering (events)

AIProviders posts events around every `complete()` and `converse()` provider call so a plugin can limit or bill AI usage (Matomo Cloud does) without AIProviders depending on it. With no listener, nothing changes.

| Event | When | Listener gets |
| --- | --- | --- |
| `AIProviders.beforeRequest` | Before every `complete()` / `converse()` provider call, after the provider is resolved | `AIRequestContext $context`, `AIRequestDecision $decision` |
| `AIProviders.usage` | After every `complete()` / `converse()` provider call: success, empty completion or error | `AIUsage $usage` |
| `AIProviders.getRemainingBudget` | When a caller calls `getRemainingBudget($featureKey)` before a batch | `string $featureKey`, `?int &$budget` |
| `AIProviders.checkFeatureAllowed` | When a caller calls `checkFeatureAllowed($featureKey, $payload)` before an action | `string $featureKey`, `array $payload`, `AIRequestDecision $decision` |

- A decision starts as allowed, and listeners can only `deny()`, so one listener cannot overrule another's denial. A denied call is never sent: the caller gets an `AIQuotaExceededException` carrying the decision.
- `beforeRequest` and `usage` share one context, whose request ID links them and can serve as a dedupe key.
- An exception thrown by a `usage` listener is logged and not passed on, because the call has already been made and paid for. It does stop the listeners after it, so catch your own errors.
- Meter `usage` events with outcome `success` only. An `error` outcome may mean nothing was sent or billed, for example when no API key is configured. `beforeRequest` is also posted by `assertRequestAllowed()` (with `$context->isProbe()` true), and no usage follows a probe or a denial.
- Connection tests and model listings in the admin UI call the provider directly and post no events.
- Neither event carries prompt or response content.

Callers help listeners by describing the call:

```php
$request = (new AIRequest($prompt, 'YourPlugin'))
    ->withFeatureKey('YourPlugin.summary') // defaults to 'YourPlugin.default'
    ->withIdSite($idSite)
    ->withUsageReference((string) $idQuery) // your own ID for this call
    ->withMeta(['source' => 'scheduled']); // extra data for listeners, no prompt content

try {
    $response = $service->complete($request);
} catch (AIQuotaExceededException $e) {
    // Show $e->getMessage(); $e->getDecision() has the used / limit figures.
}
```

`assertRequestAllowed($request)` asks the listeners the same question before a feature starts, without calling the provider. It does not check whether the provider supports the request.

Providers report what a call used on their response: input, output and prompt-cache token counts (`inputTokens` excludes cached tokens for every provider), web searches, `flatFeeCalls` for per-call priced APIs, the provider-billed `cost`, and free-form `providerMeta`.

---

### Overview (`plugins/ExamplePlugin/README.md`)

# Matomo ExamplePlugin Plugin

## Description

Add your plugin description here.

---

### Overview (`plugins/ExampleTheme/README.md`)

# Matomo ExampleTheme Theme

## Description

Add your theme description here.

---

### Overview (`plugins/ExampleVisualization/README.md`)

# Matomo SimpleTableVisualizationExample Plugin

## Description

Example for generating a simple visualization.

## FAQ

__My question?__
My answer

## Changelog

Here goes the changelog text.

## Support

Please direct any feedback to ...

---

### Overview (`node_modules/@materializecss/materialize/README.md`)

<p align="center">
  <a href="https://materializecss.github.io/materialize/">
    <img src="https://materializecss.github.io/materialize/res/materialize.svg" width="150">
  </a>
</p>

<h3 align="center">MaterializeCSS</h3>

<p align="center">
  Materialize, a CSS Framework based on material design.
  <br>
  <a href="https://materializecss.github.io/materialize/"><strong>-- Browse the docs --</strong></a>
  <br>
  <br>
  <a href="https://github.com/materializecss/materialize/actions/">
    <img src="https://github.com/materializecss/materialize/actions/workflows/nightly.yml/badge.svg" alt="GitHub Actions badge">
  </a>
  <a href="https://www.npmjs.com/package/@materializecss/materialize">
    <img src="https://badge.fury.io/js/%40materializecss%2Fmaterialize.svg" alt="npm version badge">
  </a>
  <a href="https://www.jsdelivr.com/package/npm/@materializecss/materialize">
    <img src="https://data.jsdelivr.com/v1/package/npm/@materializecss/materialize/badge" alt="jsDelivr version badge">
  </a>
  <a href="https://gitter.im/materializecss/materialize">
    <img src="https://badges.gitter.im/Join%20Chat.svg" alt="Gitter badge">
  </a>
</p>

## Table of Contents
- [Quickstart](#quickstart)
- [Documentation](#documentation)
- [Build](#build)
- [Supported Browsers](#supported-browsers)
- [Changelog](#changelog)
- [Testing](#testing)
- [Contributing](#contributing)
- [Copyright and license](#copyright-and-license)

## Quickstart:
Read the [getting started guide](https://materializecss.github.io/materialize/getting-started.html) for more information on how to use materialize.

- [Download the latest release](https://github.com/materializecss/materialize/releases/latest) of materialize directly from GitHub. ([Beta](https://github.com/materializecss/materialize/releases/))
- Clone the repo: `git clone https://github.com/materializecss/materialize.git`
- Include the files via [jsDelivr](https://www.jsdelivr.com/package/npm/@materializecss/materialize).
- Install with [npm](https://www.npmjs.com): `npm install @materializecss/materialize` (Beta: `npm install @materializecss/materialize@next`)

## Documentation
The documentation can be found at <https://materializecss.github.io/materialize>. To run the documentation locally on your machine, you need [Node.js](https://nodejs.org/en/) installed on your computer.

### Running documentation locally
Run these commands to set up the documentation:

```bash
git clone https://github.com/materializecss/materialize
cd materialize
npm install
```

Then run `npm run dev` to compile the documentation. When it finishes, open a new browser window and navigate to `localhost:8000/docs`. We use [BrowserSync](https://www.browsersync.io/) to display the documentation.

### Documentation for previous releases
Previous releases and their documentation are available for [download](https://github.com/materializecss/materialize/releases).

## Build
If you want to build `materialize.css` or `materialize.js` from the latest commit, you can build the files with the following command after `npm install`. See `package.json` to check the current version like `1.0.0`.

```sh
npm run release -- --oldver=<current_version> --newver=<new_version>
```

## Supported Browsers:
Materialize is compatible with:

- Chrome 35+
- Firefox 31+
- Safari 9+
- Opera
- Edge
- IE 11+

## Changelog
For changelogs, check out [the Releases section of materialize](https://github.com/materializecss/materialize/releases) or the [CHANGELOG.md](CHANGELOG.md).

## Testing
We use Jasmine as our testing framework and we're trying to write a robust test suite for our components. If you want to help, [here's a starting guide on how to write tests in Jasmine](CONTRIBUTING.md#jasmine-testing-guide).

## Contributing
Check out the [CONTRIBUTING document](CONTRIBUTING.md) in the root of the repository to learn how you can contribute. You can also browse the [help-wanted](https://github.com/materializecss/materialize/labels/help-wanted) tag in our issue tracker to find things to do.

## Copyright and license
Code Copyright 2023 Materialize. Code released under the MIT license.

---

### Overview (`node_modules/mousetrap/plugins/README.md`)

# Plugins

Plugins extend the functionality of Mousetrap.  To use a plugin just include the plugin after mousetrap.

```html
<script src="mousetrap.js"></script>
<script src="mousetrap-record.js"></script>
```

## Bind dictionary

Allows you to make multiple bindings in a single ``Mousetrap.bind`` call.

## Global bind

Allows you to set global bindings that work even inside of input fields.

## Pause/unpause

Allows you to temporarily prevent Mousetrap events from firing.

## Record

Allows you to capture a keyboard shortcut or sequence defined by a user.

---

### Overview (`plugins/Morpheus/fonts/README.md`)

To change / add / remove icons please read the instructions on [https://developer.matomo.org/guides/matomo-font](https://developer.matomo.org/guides/matomo-font).

---

### Overview (`node_modules/mousetrap/plugins/bind-dictionary/README.md`)

# Bind Dictionary

This extension overwrites the default bind behavior and allows you to bind multiple combinations in a single bind call.

Usage looks like:

```javascript
Mousetrap.bind({
    'a': function() { console.log('a'); },
    'b': function() { console.log('b'); }
});
```

You can optionally pass in ``keypress``, ``keydown`` or ``keyup`` as a second argument.

Other bind calls work the same way as they do by default.

---

### Overview (`node_modules/mousetrap/plugins/global-bind/README.md`)

# Global Bind

This extension allows you to specify keyboard events that will work anywhere including inside textarea/input fields.

Usage looks like:

```javascript
Mousetrap.bindGlobal('ctrl+s', function() {
    _save();
});
```

This means that a keyboard event bound using ``Mousetrap.bind`` will only work outside of form input fields, but using ``Moustrap.bindGlobal`` will work in both places.

If you wanted to create keyboard shortcuts that only work when you are inside a specific textarea you can do that too by creating your own extension.

---

### Overview (`node_modules/mousetrap/plugins/pause/README.md`)

# Pause/unpause

This extension allows Mousetrap to be paused and unpaused without having to reset keyboard shortcuts and rebind them.

Usage looks like:

```javascript
// stop Mousetrap events from firing
Mousetrap.pause();

// allow Mousetrap events to fire again
Mousetrap.unpause();
```

---

### Overview (`node_modules/mousetrap/plugins/record/README.md`)

# Record

This extension lets you use Mousetrap to record keyboard sequences and play them back:

```html
<button onclick="recordSequence()">Record</button>

<script>
    function recordSequence() {
        Mousetrap.record(function(sequence) {
            // sequence is an array like ['ctrl+k', 'c']
            alert('You pressed: ' + sequence.join(' '));
        });
    }
</script>
```

---

### Node Modules Chroma Js Doc Api (`node_modules/chroma-js/doc/api.md`)

# Initializing colors

## chroma(a, b, c, [a], [mode])

Generic color factory. Returns an instance of chroma.Color. mode defaults to "rgb".

The following calls all return the same color (red #ff0000):

```javascript
chroma("red");
chroma("#ff0000");
chroma("#f00");
chroma("FF0000");
chroma(255, 0, 0);
chroma([255, 0, 0]);
chroma(0, 1, 0.5, 'hsl');
chroma([0, 1, 0.5], 'hsl');
chroma(0, 1, 1, 'hsv');
chroma("rgb(255,0,0)");
chroma("rgb(100%,0%,0%)");
chroma("hsl(0,100%,50%)");
chroma(53.24, 80.09, 67.20, 'lab');
chroma(53.24, 104.55, 40, 'lch');
chroma(1, 0, 0, 'gl');
```


## chroma.hex() / chroma.css()

Returns a color from a hex code or css color. Alias: **chroma.css()**

```javascript
chroma.hex("#ff0000");
chroma.hex("red");
chroma.hex("rgb(255, 0, 0)");
```
## chroma.*xyz*()

Creates a chroma.Color instance from a specific color space. Shortcut to *chroma(…, mode)*.

```javascript
chroma.rgb(255, 0, 0);
chroma.hsl(0, 1, 0.5);
chroma.hsv(120, 0.5, 0.5);
chroma.lab(53.24, 80.09, 67.20);
chroma.lch(53.24, 104.55, 40);
chroma.gl(1, 0, 0);
```

## chroma.interpolate(color1, color2, f, mode)

Colors can be also be interpolates between two other colors in a given mode.

```
chroma.interpolate('white', 'black', 0)  // #ffffff
chroma.interpolate('white', 'black', 1)  // #000000
chroma.interpolate('white', 'black', 0.5)  // #7f7f7f
chroma.interpolate('white', 'black', 0.5, 'hsv')  // #808080
chroma.interpolate('white', 'black', 0.5, 'lab')  // #777777
```

This also works with colors with alpha channel:

```
chroma.interpolate('rgba(0,0,0,0)', 'rgba(255,0,0,1)', 0.5).css()  //"rgba(127.5,0,0,0.5)"
```

## chroma.interpolate.bezier(colors)

Colors can be also be interpolates between two other colors in a given mode.

```
bezInterpolator = chroma.interpolate.bezier(['white', 'yellow', 'red', 'black']);
bezInterpolator(0).hex()  // #ffffff
bezInterpolator(0.33).hex()  // #ffcc67
bezInterpolator(0.66).hex()  // #b65f1a
bezInterpolator(1).hex()  // #000000
```

# Working with chroma.colors

Here's what you can do with it:

* [color.hex|css|rgb|hsv|hsl|lab|lch()](#colorxxx)
* [color.alpha()](#coloralpha)
* [color.darker()](#colordarkeramount)
* [color.brighter()](#colorbrighteramount)
* [color.saturate()](#colorsaturateamount)
* [color.desaturate()](#colordesaturateamount)
* [color.luminance()](#colorluminance)

### color.*xxx*()

Returns the color components for a specific color space:

```javascript
chroma('red').hex()  // "#FF0000""
chroma('red').rgb()  // [255, 0, 0]
chroma('red').hsv()  // [0, 1, 1]
chroma('red').hsl()  // [0, 1, 0.5]
chroma('red').lab()  // [53.2407, 80.0924, 67.2031]
chroma('red').lch()  // [53.2407, 104.5517, 39.9990]
chroma('red').rgba() // [255, 0, 0, 1]
chroma('red').css()  // "rgb(255,0,0)"
chroma('red').alpha(0.7).css()  // "rgba(255,0,0,0.7)"
chroma('red').css('hsl')        // "hsl(0,100%,50%)"
chroma('red').alpha(0.7).css('hsl')  // "hsla(0,100%,50%,0.7)"
chroma('blue').css('hsla') // "hsla(240,100%,50%,1)"
```

### color.alpha()

Returns or sets the colors alpha value.

```
var red = chroma('red');
red.alpha(0.5);
red.css();  // rgba(255,0,0,0.5);
```

### color.darker(*amount*)

Decreases the lightness of the color in *Lab* color space.

```javascript
chroma('red').darken().hex()  // #BC0000
```

### color.brighter(*amount*)

```javascript
chroma('red').brighten().hex()  // #FF603B
```

### color.saturate(*amount*)

Returns a more saturated variation of the color.

```javascript
chroma('#eecc99').saturate().hex() // #fcc973
```

### color.desaturate(*amount*)

Returns a less saturated variation of the color.

```javascript
chroma('red').desaturate().hex() // #ec3d23
```

### color.luminance()

Returns the [relative luminance](http://www.w3.org/TR/WCAG20/#relativeluminancedef) of the color, which is a value between 0 (black) and 1 (white).

```javascript
chroma('black').luminance() // 0
chroma('white').luminance() // 1
chroma('red').luminance() // 0.2126
```

As of version 0.6.2 you can also set the luminance directly:

```javascript
chroma('#ff0000').luminance(0.4).hex() // #ff8585"
```

# Working with color scales

## chroma.scale()

Creates a color scale function from the given set of colors.

```javascript
var scale = chroma.scale(['lightyellow', 'navy']);
scale(0.5);  // #7F7FB0
```

Need some advice for good colors? How about using a pre-defined [ColorBrewer](http://colorbrewer2.com) scale:

```javascript
chroma.scale('RdYlBu');
```

### scale.out()

By default the color scale functions return instances of chroma.Color.

```javascript
var col = scale(0.5);
col.hex();  // #7F7FB0
col.rgb();  // [127.5, 127.5, 176]
```

Using **scale.out()** you can configure the color scale to automatically return colors in the desired format.

```javascript
scale = chroma.scale(['lightyellow', 'navy']).out('hex');
scale(0.5);  // "#7F7FB0"
```

### scale.mode()

Specify in which color space the colors should be interpolated. Defaults to "rgb". You can use any of the following spaces:

```javascript
var scale = chroma.scale(['lightyellow', 'navy']);
scale.mode('hsv')(0.5);  // #54C08A
scale.mode('hsl')(0.5);  // #31FF98
scale.mode('lab')(0.5);  // #967CB2
scale.mode('lch')(0.5);  // #D26662
```

### scale.domain()

You can specify the input range of your data (defaults to [0..1]).

```javascript
var scale = chroma.scale(['lightyellow', 'navy']).domain([0, 400]);
scale(200);  // #7F7FB0
```

Instead of just passing the minimum and maximum values you can specify custom "stops". chroma.js would now return a distinct set of four different colors:

```javascript
var scale = chroma.scale(['lightyellow', 'navy'])
.domain([0, 100, 200, 300, 400]);
scale(98);  // #7F7FB0
scale(99);  // #7F7FB0
scale(100);  // #AAAAC0
scale(101);  // #AAAAC0
```

If you don't want to pick the stops by hand, you can auto-generate a set of *N* equidistant input classes:

```javascript
chroma.scale(['#eee', '#900']).domain([0, 400], 7);
```

Don't like linear scales? How about logarithmic stops?

```javascript
chroma.scale(['#eee', '#900']).domain([1, 1000000], 7, 'log');
```

For more advanced techniques you need the actual dataset

```javascript
chroma.scale(['#eee', '#900']).domain(values, 5, 'quantiles');
chroma.scale(['#eee', '#900']).domain(values, 5, 'k-means');
```

Calling .domain() with no arguments will return the current domain.

```
chroma.scale(['white', 'red']).domain([0, 100], 4).domain() // [0, 25, 50, 75, 100]
```

### scale.range()

If you need to change the color range after initializing the color scale.

```javascript
chroma.scale().range(['lightyellow', 'navy']);
```

### scale.correctLightness()

As of version 0.5.2 chroma.scale supports automatic lightness correction of color scales.

**Important note:** The lightness correction only works for sequential color scales, where the input colors are ordered by lightness. So this won’t work for diverging color scales, yet.

```javascript
chroma.scale(['lightyellow', 'navy']).correctLightness(true);
```

### scale.colors(mode='hex')

If your color scale has set a distinct number of classes, scale.colors() can be used to retreive all possible colors generated by this scale.

```javascript
chroma.scale('RdYlGn').domain([0,1], 5).colors()
// returns ['#a50026', '#f88d52', '#ffffbf', '#86cb66', '#006837']
```

# Useful methods

## chroma.luminance

Shortcut for the color.luminance()

```javascript
chroma.luminance('black') // 0
chroma.luminance('white') // 1
chroma.luminance('#ff0000') // 0.2126
```

## chroma.contrast(a, b)

Returns the [contrast ratio](http://www.w3.org/TR/WCAG20/#contrast-ratiodef) between two given colors. According to the [Web Content Accessibility Guidelines](http://www.w3.org/TR/WCAG20) the contrast between background and small text [should be at least](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast) 4.5 : 1.

```javascript
chroma.contrast('white', 'navy')  // 16.00 – ok
chroma.contrast('white', 'yellow')  // 1.07 – not ok!
```

---

### Plugins CustomDimensions Docs Faq (`plugins/CustomDimensions/docs/faq.md`)

## FAQ

__I have a large database, can I install the plugin on the command line?__

Yes, this is not only possible but even recommended as the installation may take hours. To do this:

* Execute the command `./console plugin:activate CustomDimensions` within your Matomo directory

__Where can I manage Custom Dimensions?__

Custom Dimensions can be managed by clicking on your username or user icon in the top right. There will be a menu
item "Custom Dimensions" within the "Manage" section of the left menu. By clicking on it you can manage Custom Dimensions.
Please note that the permission Admin is required in order to be able to manage them.

__Where can I find the Id for a Custom Dimension?__

You can find them by going to the "Manage Custom Dimensions" page in your personal area. For each dimension you will
find the Id in the table that lists all available Custom Dimensions.

__How do I set a value for a dimension in the JavaScript Tracker?__

Please have a look at the [JavaScript Tracker guide for Custom Dimensions](https://developer.matomo.org/guides/tracking-javascript-guide#custom-dimensions).

__How do I set a value for a dimension in the PHP Tracker?__

`$tracker->setCustomDimension($customDimensionId, $value);`

Please note custom tracking parameters are cleared after each tracking request. If you want to keep the same
Custom Dimensions over all request make sure to call this method before each tracking call.

__I have configured all available Custom Dimension slots, can I add more?__

Yes, this is possible. To make a new Custom Dimension slot available execute the following command including the scope option:

```
./console customdimensions:add-custom-dimension --scope=action
./console customdimensions:add-custom-dimension --scope=visit
```

Be aware that this can take a long time depending on the size of your database as it requires MySQL schema changes.
You can directly create multiple Custom Dimension slots. To do this add the option `--count=X`. Usually it doesn't take much
longer to create directly multiple new slots.

__Is it possible to delete a Custom Dimension and all of its data?__

In the UI it is only possible to deactivate a dimension. However, on the command line you can remove a Custom Dimension
and report it's log data by executing the following console command:

```
./console customdimensions:remove-custom-dimension --scope=$scope --index=$index
```

Make sure to replace `$scope` and `$index` with the correct values. To get a list of all available indexes execute `./console customdimensions:info`.

Removing a Custom Dimension may take a long time as it requires MySQL schema changes. Currently, only log data is removed. Archived reports will be
not deleted currently.

---

### Plugins TrackingSpamPrevention Docs Faq (`plugins/TrackingSpamPrevention/docs/faq.md`)

## FAQ

__How do I allow specific IPs to not be blocked?__

Say you are using AWS to replay your traffic using log analytics. When you have cloud provider IP range blocking enabled, all the requests from your AWS would be blocked. However, you can specifically allow your own IPs to be allowed and not blocked using the "IP allow list" setting in "Administration => General Settings". Enter one IP address or CIDR range per line.

Alternatively, you can force a list of allowed IP ranges by editing your `config/config.ini.php` file like this (this overrides the setting, and the field is hidden from the UI while the override is in place):

```
[TrackingSpamPrevention]
ip_allow_list[] = "127.0.0.1/32"
ip_allow_list[] = "192.168.0.0/21"
```

Make sure to enter a valid IP range. 

__How do I block specific IPs from being tracked?__

Use the "IP block list" setting in "Administration => General Settings". Enter one IP address or CIDR range per line. If an address matches both the allow list and the block list, the allow list takes precedence.

Alternatively, you can force a list of blocked IP ranges by editing your `config/config.ini.php` file like this (this overrides the setting, and the field is hidden from the UI while the override is in place):

```
[TrackingSpamPrevention]
ip_block_list[] = "203.0.113.88/32"
ip_block_list[] = "198.51.100.0/24"
```

__What happens when it fails to synchronise public IPs from cloud providers?__

Any error is currently ignored and if it does not synchronise successfully, then the IP for the provider that failed are not synced.

To be aware when such an error happens you can enable the following setting:

```
[TrackingSpamPrevention]
block_cloud_sync_throw_exception_on_error = 1
```

It is disabled by default as it could stop other scheduled tasks from being executed.

__How can I block specific organisations from being tracked?__

This can be useful if you are receiving spam requests from a provider that isn't automatically detected yet by this plugin.

For this to work a geolocation provider must be enabled, and "Block tracking requests from Cloud hosting providers" in "Administration => General Settings" must be set to "Use custom organisation list". The "Organisation block list" starts from whatever list is already stored for your installation, which is Matomo's default set of hosting and datacenter providers until something changes it. Extend or trim it as needed. Note that a stored list stops following Matomo's maintained one, so if you find yourself on this option after upgrading and did not choose it, selecting "Use Matomo's default provider list" puts you back on the list Matomo keeps up to date.

The other two options need no list of your own: "Use Matomo's default provider list" always matches against the current default set, and "Do not block Cloud hosting providers" turns organisation matching off. Your custom list is kept either way, so it is still there when you switch back.

Alternatively, you can execute a command to block a new organisation like this:

```bash
./console trackingspamprevention:block-geo-ip-organisation --organisation-name="Example"
```

When "Use Matomo's default provider list" is selected the command switches to the custom organisation list, so that the organisation it adds is actually matched. When blocking is turned off it leaves that alone and tells you the entry has no effect until you select the custom list.

You can also force a list of blocked organisations by editing your `config/config.ini.php` file like this (this overrides the setting, and the field is hidden from the UI while the override is in place):

```
[TrackingSpamPrevention]
organisation_block_list[] = "ExampleOrg"
organisation_block_list[] = "another example"
```

An overridden list is used whenever organisation blocking is on, including under "Use Matomo's default provider list".

Each organisation will be compared lower case and the organisation only needs to contain the configured value, it does not need to match it exactly.

You can find out the organisation name for an IP address by visiting the website of your geolocation database and using their demo tool.

---

--- METRICS ---
- Files Extracted: 31
- Estimated Token Budget: ~25593 tokens
- Canonical Reference: https://codewiki.google/github.com/matomo-org/matomo
