[
  {
    "path": ".eslintignore",
    "content": "dist/\nnode_modules/\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"root\": true,\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\n    \"@typescript-eslint\",\n    \"prettier\"\n  ],\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/eslint-recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"prettier\"\n  ],\n  \"rules\": {\n    \"prefer-rest-params\": \"off\",\n    \"prettier/prettier\": [\"error\"],\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n    \"@typescript-eslint/no-non-null-assertion\": \"off\",\n    \"@typescript-eslint/no-empty-function\": \"off\",\n    \"@typescript-eslint/no-unused-vars\": [\"error\", { \"argsIgnorePattern\": \"^_\" }],\n    \"@typescript-eslint/ban-types\": [\"error\", {\n      \"types\": {\n        \"Function\": false,\n        \"Object\": false,\n        \"{}\": false\n      }\n    }]\n  }\n}\n"
  },
  {
    "path": ".github/scripts/publish-dev-build",
    "content": "#!/usr/bin/env bash\nset -eux\n\nDEV_BUILD_REPO_NAME=\"hotwired/dev-builds\"\nDEV_BUILD_ORIGIN_URL=\"https://${1}@github.com/${DEV_BUILD_REPO_NAME}.git\"\nBUILD_PATH=\"$HOME/publish-dev-build\"\n\nmkdir \"$BUILD_PATH\"\n\ncd \"$GITHUB_WORKSPACE\"\npackage_name=\"$(jq -r .name package.json)\"\npackage_files=( dist package.json )\ntag=\"${package_name}/${GITHUB_SHA:0:7}\"\n\nname=\"$(git log -n 1 --format=format:%cn)\"\nemail=\"$(git log -n 1 --format=format:%ce)\"\nsubject=\"$(git log -n 1 --format=format:%s)\"\ndate=\"$(git log -n 1 --format=format:%ai)\"\nurl=\"https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}\"\nmessage=\"$tag $subject\"$'\\n\\n'\"$url\"\n\ncp -R \"${package_files[@]}\" \"$BUILD_PATH\"\n\ncd \"$BUILD_PATH\"\ngit init .\ngit remote add origin \"$DEV_BUILD_ORIGIN_URL\"\ngit symbolic-ref HEAD refs/heads/publish-dev-build\ngit add \"${package_files[@]}\"\n\nGIT_AUTHOR_DATE=\"$date\" GIT_COMMITTER_DATE=\"$date\" \\\nGIT_AUTHOR_NAME=\"$name\" GIT_COMMITTER_NAME=\"$name\" \\\nGIT_AUTHOR_EMAIL=\"$email\" GIT_COMMITTER_EMAIL=\"$email\" \\\n  git commit -m \"$message\"\n\ngit tag \"$tag\"\n[ \"$GITHUB_REF\" != \"refs/heads/main\" ] || git tag -f \"${package_name}/latest\"\ngit push -f --tags\n\necho done\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non: [push, pull_request]\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node: [18, 19, 20, 21]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Node v${{ matrix.node }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node }}\n          cache: 'yarn'\n\n      - name: Install Dependencies\n        run: yarn install --frozen-lockfile\n\n      - name: Build\n        run: yarn build\n\n      - name: Test Build\n        run: yarn build:test\n"
  },
  {
    "path": ".github/workflows/dev-builds.yml",
    "content": "name: dev-builds\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - 'builds/**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'yarn'\n\n      - run: yarn install\n      - run: yarn build\n\n      - name: Publish dev build\n        run: .github/scripts/publish-dev-build '${{ secrets.DEV_BUILD_GITHUB_TOKEN }}'\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 21\n          cache: 'yarn'\n\n      - name: Install Dependencies\n        run: yarn install --frozen-lockfile\n\n      - name: Lint\n        run: yarn lint\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non: [push, pull_request]\n\njobs:\n  test:\n    name: Test\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: browser-actions/setup-chrome@v1\n      - uses: browser-actions/setup-firefox@v1\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: 'yarn'\n\n      - name: Install Dependencies\n        run: yarn install --frozen-lockfile\n\n      - name: Test\n        run: yarn test\n"
  },
  {
    "path": ".gitignore",
    "content": "coverage/\ndist/\nnode_modules/\ndocs/api/\n*.log\n*.tsbuildinfo\n"
  },
  {
    "path": ".node-version",
    "content": "20.11.0\n"
  },
  {
    "path": ".npmignore",
    "content": "src/tests/\ndist/tests/\ntsconfig*\n*.log\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist/\nnode_modules/\n"
  },
  {
    "path": ".prettierrc.json",
    "content": " {\n   \"singleQuote\": false,\n   \"printWidth\": 120,\n   \"semi\": false\n }\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nPlease see [our GitHub \"Releases\" page](https://github.com/hotwired/stimulus/releases).\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\neducation, socio-economic status, nationality, personal appearance, race,\nreligion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at <opensource@basecamp.com>. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": "LICENSE.md",
    "content": "# MIT License\n\nCopyright © 2021 Basecamp, LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nThe Software is provided \"as is,\" without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.\n"
  },
  {
    "path": "README.md",
    "content": "# <img src=\"assets/logo.svg?sanitize=true\" width=\"24\" height=\"24\" alt=\"Stimulus\"> Stimulus\n\n### A modest JavaScript framework for the HTML you already have\n\nStimulus is a JavaScript framework with modest ambitions. It doesn't seek to take over your entire front-end—in fact, it's not concerned with rendering HTML at all. Instead, it's designed to augment your HTML with just enough behavior to make it shine. Stimulus pairs beautifully with [Turbo](https://turbo.hotwired.dev) to provide a complete solution for fast, compelling applications with a minimal amount of effort.\n\nHow does it work? Sprinkle your HTML with controller, target, and action attributes:\n\n```html\n<div data-controller=\"hello\">\n  <input data-hello-target=\"name\" type=\"text\">\n\n  <button data-action=\"click->hello#greet\">Greet</button>\n\n  <span data-hello-target=\"output\"></span>\n</div>\n```\n\nThen write a compatible controller. Stimulus brings it to life automatically:\n\n```js\n// hello_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"name\", \"output\" ]\n\n  greet() {\n    this.outputTarget.textContent =\n      `Hello, ${this.nameTarget.value}!`\n  }\n}\n```\n\nStimulus continuously watches the page, kicking in as soon as attributes appear or disappear. It works with any update to the DOM, regardless of whether it comes from a full page load, a [Turbo](https://turbo.hotwired.dev) page change, or an Ajax request. Stimulus manages the whole lifecycle.\n\nYou can write your first controller in five minutes by following along in the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction).\n\nYou can read more about why we created this new framework in [The Origin of Stimulus](https://stimulus.hotwired.dev/handbook/origin).\n\n## Installing Stimulus\n\nYou can use Stimulus with any asset packaging systems. And if you prefer no build step at all, just drop a `<script>` tag on the page and get right down to business.\n\nSee the [Installation Guide](https://stimulus.hotwired.dev/handbook/installing) for detailed instructions.\n\n## Getting Help\n\nLooking for the docs? Once you've read through the Handbook, consult the [Stimulus Reference](https://stimulus.hotwired.dev/reference/controllers) for API details.\n\nHave a question about Stimulus? Connect with other Stimulus developers on the [Hotwire Discourse](https://discuss.hotwired.dev/) community forum.\n\n## Contributing Back\n\nFind a bug? Head over to our [issue tracker](https://github.com/hotwired/stimulus/issues) and we'll do our best to help. We love pull requests, too!\n\nWe expect all Stimulus contributors to abide by the terms of our [Code of Conduct](CODE_OF_CONDUCT.md).\n\n### Development\n\n- Fork the project locally\n- `yarn install`\n- `yarn start` - to run the local dev server with examples\n- `yarn test` - to run the unit tests\n- `yarn lint` - to run the linter with ESLint\n- `yarn format` - to format changes with Prettier\n\n## Acknowledgments\n\nStimulus is [MIT-licensed](LICENSE.md) open-source software from [Basecamp](https://basecamp.com/), the creators of [Ruby on Rails](https://rubyonrails.org).\n\n---\n\n© 2024 Basecamp, LLC.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Considerations\n\n### Q: Can I be confident that if my cross-site scripting countermeasures fail, there is no way for an attacker to run arbitrary JavaScript using Stimulus?\nA: While there is no way for an attacker to run arbitrary JavaScript using Stimulus, if an attacker can insert or modify DOM elements on your page, they can use the `data-action` attribute to [define an action](docs/reference/actions.md) that invokes an arbitrary method on one of your controllers in response to an event.\n\n### Q: If an attacker manages to make changes to my application's DOM, how can I ensure that what they can do is limited if I use Stimulus?\nA: Ensure that none of the methods on any of your controllers perform sensitive operations without appropriate safeguards.\n\nRefer to the MDN [Content Security Policy documentation](https://content-security-policy.com) for a general overview of cross-site scripting attacks and how to defend against them.\n\n### Q: Will Stimulus only instantiate and invoke methods on classes marked as controllers?\nA: Yes. All controller classes must be registered with corresponding identifiers, either implicitly by way of an autoloader like used in [stimulus-rails](https://github.com/hotwired/stimulus-rails) with import maps or explicitly through a call to `Application#register()`.\n\n### Q: Does Stimulus use `eval()`?\nA: No. There is no use of `eval()` in Stimulus. The action system _does_ use dynamic dispatch to invoke controller methods, which corresponds to a runtime property lookup on the controller instance. See the implementation of [`Binding#method`](src/core/binding.ts) for details.\n"
  },
  {
    "path": "docs/handbook/00_the_origin_of_stimulus.md",
    "content": "---\npermalink: /handbook/origin.html\nnav_prefix: Preface\norder: 00\n---\n\n# The Origin of Stimulus\n\nWe write a lot of JavaScript at [Basecamp](https://basecamp.com), but we don’t use it to create “JavaScript applications” in the contemporary sense. All our applications have server-side rendered HTML at their core, then add sprinkles of JavaScript to make them sparkle.\n\nThis is the way of the [majestic monolith](https://signalvnoise.com/svn3/the-majestic-monolith/). Basecamp runs across half a dozen platforms, including native mobile apps, with a single set of controllers, views, and models created using Ruby on Rails. Having a single, shared interface that can be updated in a single place is key to being able to perform with a small team, despite the many platforms.\n\nIt allows us to party with productivity like days of yore. A throwback to when a single programmer could make rapacious progress without getting stuck in layers of indirection or distributed systems. A time before everyone thought the holy grail was to confine their server-side application to producing JSON for a JavaScript-based client application.\n\nThat’s not to say that there isn’t value in such an approach for some people, some of the time. Just that as a general approach to many applications, and certainly the likes of Basecamp, it’s a regression in overall simplicity and productivity.\n\nAnd it’s also not to say that the proliferation of single-page JavaScript applications hasn’t brought real benefits. Chief amongst which has been faster, more fluid interfaces set free from the full-page refresh.\n\nWe wanted Basecamp to feel like that too. As though we had followed the herd and rewritten everything with client-side rendering or gone full-native on mobile.\n\nThis desire led us to a two-punch solution: [Turbo](https://turbo.hotwired.dev) and Stimulus.\n\n### Turbo up high, Stimulus down low\n\nBefore I get to Stimulus, our new modest JavaScript framework, allow me to recap the proposition of Turbo.\n\nTurbo descends from an approach called [pjax](https://github.com/defunkt/jquery-pjax), developed at GitHub. The basic concept remains the same. The reason full-page refreshes often feel slow is not so much because the browser has to process a bunch of HTML sent from a server. Browsers are really good and really fast at that. And in most cases, the fact that an HTML payload tends to be larger than a JSON payload doesn’t matter either (especially with gzipping). No, the reason is that CSS and JavaScript has to be reinitialized and reapplied to the page again. Regardless of whether the files themselves are cached. This can be pretty slow if you have a fair amount of CSS and JavaScript.\n\nTo get around this reinitialization, Turbo maintains a persistent process, just like single-page applications do. But largely an invisible one. It intercepts links and loads new pages via Ajax. The server still returns fully-formed HTML documents.\n\nThis strategy alone can make most actions in most applications feel really fast (if they’re able to return server responses in 100-200ms, which is eminently possible with caching). For Basecamp, it sped up the page-to-page transition by ~3x. It gives the application that feel of responsiveness and fluidity that was a massive part of the appeal for single-page applications.\n\nBut Turbo alone is only half the story. The coarsely grained one. Below the grade of a full page change lies all the fine-grained fidelity within a single page. The behavior that shows and hides elements, copies content to a clipboard, adds a new todo to a list, and all the other interactions we associate with a modern web application.\n\nPrior to Stimulus, Basecamp used a smattering of different styles and patterns to apply these sprinkles. Some code was just a pinch of jQuery, some code was a similarly sized pinch of vanilla JavaScript, and some again was larger object-oriented subsystems. They all usually worked off explicit event handling hanging off a `data-behavior` attribute.\n\nWhile it was easy to add new code like this, it wasn’t a comprehensive solution, and we had too many in-house styles and patterns coexisting. That made it hard to reuse code, and it made it hard for new developers to learn a consistent approach.\n\n### The three core concepts in Stimulus\n\nStimulus rolls up the best of those patterns into a modest, small framework revolving around just three main concepts: Controllers, actions, and targets.\n\nIt’s designed to read as a progressive enhancement when you look at the HTML it’s addressing. Such that you can look at a single template and know which behavior is acting upon it. Here’s an example:\n\n```html\n<div data-controller=\"clipboard\">\n  PIN: <input data-clipboard-target=\"source\" type=\"text\" value=\"1234\" readonly>\n  <button data-action=\"clipboard#copy\">Copy to Clipboard</button>\n</div>\n```\n\nYou can read that and have a pretty good idea of what’s going on. Even without knowing anything about Stimulus or looking at the controller code itself. It’s almost like pseudocode. That’s very different from reading a slice of HTML that has an external JavaScript file apply event handlers to it. It also maintains the separation of concerns that has been lost in many contemporary JavaScript frameworks.\n\nAs you can see, Stimulus doesn’t bother itself with creating the HTML. Rather, it attaches itself to an existing HTML document. The HTML is, in the majority of cases, rendered on the server either on the page load (first hit or via Turbo) or via an Ajax request that changes the DOM.\n\nStimulus is concerned with manipulating this existing HTML document. Sometimes that means adding a CSS class that hides an element or animates it or highlights it. Sometimes it means rearranging elements in groupings. Sometimes it means manipulating the content of an element, like when we transform UTC times that can be cached into local times that can be displayed.\n\nThere are cases where you’d want Stimulus to create new DOM elements, and you’re definitely free to do that. We might even add some sugar to make it easier in the future. But it’s the minority use case. The focus is on manipulating, not creating elements.\n\n\n### How Stimulus differs from mainstream JavaScript frameworks\n\nThis makes Stimulus very different from the majority of contemporary JavaScript frameworks. Almost all are focused on turning JSON into DOM elements via a template language of some sort. Many use these frameworks to birth an empty page, which is then filled exclusively with elements created through this JSON-to-template rendering.\n\nStimulus also differs on the question of state. Most frameworks have ways of maintaining state within JavaScript objects, and then render HTML based on that state. Stimulus is the exact opposite. State is stored in the HTML, so that controllers can be discarded between page changes, but still reinitialize as they were when the cached HTML appears again.\n\nIt really is a remarkably different paradigm. One that I’m sure many veteran JavaScript developers who’ve been used to work with contemporary frameworks will scoff at. And hey, scoff away. If you’re happy with the complexity and effort it takes to maintain an application within the maelstrom of, say, React + Redux, then Turbo + Stimulus will not appeal to you.\n\nIf, on the other hand, you have nagging sense that what you’re working on does not warrant the intense complexity and application separation such contemporary techniques imply, then you’re likely to find refuge in our approach.\n\n\n### Stimulus and related ideas were extracted from the wild\n\nAt Basecamp we’ve used this architecture across several different versions of Basecamp and other applications for years. GitHub has used a similar approach to great effect. This is not only a valid alternative to the mainstream understanding of what a “modern” web application looks like, it’s an incredibly compelling one.\n\nIn fact, it feels like the same kind of secret sauce we had at Basecamp when we developed [Ruby on Rails](https://rubyonrails.org/). The sense that contemporary mainstream approaches are needlessly convoluted, and that we can do more, faster, with far less.\n\nFurthermore, you don’t even have to choose. Stimulus and Turbo work great in conjunction with other, heavier approaches. If 80% of your application does not warrant the big rig, consider using our two-pack punch for that. Then roll out the heavy machinery for the part of your application that can really benefit from it.\n\nAt Basecamp, we have and do use several heavier-duty approaches when the occasion calls for it. Our calendars tend to use client-side rendering. Our text editor is [Trix](https://trix-editor.org/), a fully formed text processor that wouldn’t make sense as a set of Stimulus controllers.\n\nThis set of alternative frameworks is about avoiding the heavy lifting as much as possible. To stay within the request-response paradigm for all the many, many interactions that work well with that simple model. Then reaching for the expensive tooling when there’s a call for peak fidelity.\n\nAbove all, it’s a toolkit for small teams who want to compete on fidelity and reach with much larger teams using more laborious, mainstream approaches.\n\nGive it a go.\n\n---\n\nDavid Heinemeier Hansson\n"
  },
  {
    "path": "docs/handbook/01_introduction.md",
    "content": "---\npermalink: /handbook/introduction.html\norder: 01\n---\n\n# Introduction\n\n## About Stimulus\n\nStimulus is a JavaScript framework with modest ambitions. Unlike other front-end frameworks, Stimulus is designed to enhance _static_ or _server-rendered_ HTML—the \"HTML you already have\"—by connecting JavaScript objects to elements on the page using simple annotations.\n\nThese JavaScript objects are called _controllers_, and Stimulus continuously monitors the page waiting for HTML `data-controller` attributes to appear. For each attribute, Stimulus looks at the attribute's value to find a corresponding controller class, creates a new instance of that class, and connects it to the element.\n\nYou can think of it this way: just like the `class` attribute is a bridge connecting HTML to CSS, Stimulus's `data-controller` attribute is a bridge connecting HTML to JavaScript.\n\nAside from controllers, the three other major Stimulus concepts are:\n\n* _actions_, which connect controller methods to DOM events using `data-action` attributes\n* _targets_, which locate elements of significance within a controller\n* _values_, which read, write, and observe data attributes on the controller's element\n\nStimulus's use of data attributes helps separate content from behavior in the same way CSS separates content from presentation. Further, Stimulus's conventions naturally encourage you to group related code by name.\n\nIn turn, Stimulus helps you build small, reusable controllers, giving you just enough structure to keep your code from devolving into \"JavaScript soup.\"\n\n## About This Book\n\nThis handbook will guide you through Stimulus's core concepts by demonstrating how to write several fully functional controllers. Each chapter builds on the one before it; from start to finish, you'll learn how to:\n\n* print a greeting addressed to the name in a text field\n* copy text from a text field to the system clipboard when a button is clicked\n* navigate through a slide show with multiple slides\n* fetch HTML from the server into an element on the page automatically\n* set up Stimulus in your own application\n\nOnce you've completed the exercises here, you may find the [reference documentation](../reference/controllers) helpful for understanding technical details about the Stimulus API.\n\nLet's get started!\n"
  },
  {
    "path": "docs/handbook/02_hello_stimulus.md",
    "content": "---\npermalink: /handbook/hello-stimulus.html\norder: 02\n---\n\n# Hello, Stimulus\n\nThe best way to learn how Stimulus works is to build a simple controller. This chapter will show you how.\n\n## Prerequisites\n\nTo follow along, you'll need a running copy of the [`stimulus-starter`](https://github.com/hotwired/stimulus-starter) project, which is a preconfigured blank slate for exploring Stimulus.\n\nWe recommend [remixing `stimulus-starter` on Glitch](https://glitch.com/edit/#!/import/git?url=https://github.com/hotwired/stimulus-starter.git) so you can work entirely in your browser without installing anything:\n\n[![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/import/git?url=https://github.com/hotwired/stimulus-starter.git)\n\nOr, if you'd prefer to work from the comfort of your own text editor, you'll need to clone and set up `stimulus-starter`:\n\n```\n$ git clone https://github.com/hotwired/stimulus-starter.git\n$ cd stimulus-starter\n$ yarn install\n$ yarn start\n```\n\nThen visit http://localhost:9000/ in your browser.\n\n(Note that the `stimulus-starter` project uses the [Yarn package manager](https://yarnpkg.com/) for dependency management, so make sure you have that installed first.)\n\n## It All Starts With HTML\n\nLet's begin with a simple exercise using a text field and a button. When you click the button, we'll display the value of the text field in the console.\n\nEvery Stimulus project starts with HTML. Open `public/index.html` and add the following markup just after the opening `<body>` tag:\n\n```html\n<div>\n  <input type=\"text\">\n  <button>Greet</button>\n</div>\n```\n\nReload the page in your browser and you should see the text field and button.\n\n## Controllers Bring HTML to Life\n\nAt its core, Stimulus's purpose is to automatically connect DOM elements to JavaScript objects. Those objects are called _controllers_.\n\nLet's create our first controller by extending the framework's built-in `Controller` class. Create a new file named `hello_controller.js` in the `src/controllers/` folder. Then place the following code inside:\n\n```js\n// src/controllers/hello_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n}\n```\n\n## Identifiers Link Controllers With the DOM\n\nNext, we need to tell Stimulus how this controller should be connected to our HTML. We do this by placing an _identifier_ in the `data-controller` attribute on our `<div>`:\n\n```html\n<div data-controller=\"hello\">\n  <input type=\"text\">\n  <button>Greet</button>\n</div>\n```\n\nIdentifiers serve as the link between elements and controllers. In this case, the identifier `hello` tells Stimulus to create an instance of the controller class in `hello_controller.js`. You can learn more about how automatic controller loading works in the [Installation Guide](/handbook/installing).\n\n## Is This Thing On?\n\nReload the page in your browser and you'll see that nothing has changed. How do we know whether our controller is working or not?\n\nOne way is to put a log statement in the `connect()` method, which Stimulus calls each time a controller is connected to the document.\n\nImplement the `connect()` method in `hello_controller.js` as follows:\n```js\n// src/controllers/hello_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  connect() {\n    console.log(\"Hello, Stimulus!\", this.element)\n  }\n}\n```\n\nReload the page again and open the developer console. You should see `Hello, Stimulus!` followed by a representation of our `<div>`.\n\n## Actions Respond to DOM Events\n\nNow let's see how to change the code so our log message appears when we click the \"Greet\" button instead.\n\nStart by renaming `connect()` to `greet()`:\n\n```js\n// src/controllers/hello_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  greet() {\n    console.log(\"Hello, Stimulus!\", this.element)\n  }\n}\n```\n\nWe want to call the `greet()` method when the button's `click` event is triggered. In Stimulus, controller methods which handle events are called _action methods_.\n\nTo connect our action method to the button's `click` event, open `public/index.html` and add a `data-action` attribute to the button:\n\n```html\n<div data-controller=\"hello\">\n  <input type=\"text\">\n  <button data-action=\"click->hello#greet\">Greet</button>\n</div>\n```\n\n> ### Action Descriptors Explained\n>\n> The `data-action` value `click->hello#greet` is called an _action descriptor_. This particular descriptor says:\n> * `click` is the event name\n> * `hello` is the controller identifier\n> * `greet` is the name of the method to invoke\n\nLoad the page in your browser and open the developer console. You should see the log message appear when you click the \"Greet\" button.\n\n## Targets Map Important Elements To Controller Properties\n\nWe'll finish the exercise by changing our action to say hello to whatever name we've typed in the text field.\n\nIn order to do that, first we need a reference to the input element inside our controller. Then we can read the `value` property to get its contents.\n\nStimulus lets us mark important child elements as _targets_ so we can easily reference them in the controller through corresponding properties. Open `public/index.html` and add a `data-hello-target` attribute to the input element:\n\n```html\n<div data-controller=\"hello\">\n  <input data-hello-target=\"name\" type=\"text\">\n  <button data-action=\"click->hello#greet\">Greet</button>\n</div>\n```\n\nNext, we'll create a property for the target by adding `name` to our controller's list of target definitions. Stimulus will automatically create a `this.nameTarget` property which returns the first matching target element. We can use this property to read the element's `value` and build our greeting string.\n\nLet's try it out. Open `hello_controller.js` and update it like so:\n\n```js\n// src/controllers/hello_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"name\" ]\n\n  greet() {\n    const element = this.nameTarget\n    const name = element.value\n    console.log(`Hello, ${name}!`)\n  }\n}\n```\n\nThen reload the page in your browser and open the developer console. Enter your name in the input field and click the \"Greet\" button. Hello, world!\n\n## Controllers Simplify Refactoring\n\nWe've seen that Stimulus controllers are instances of JavaScript classes whose methods can act as event handlers.\n\nThat means we have an arsenal of standard refactoring techniques at our disposal. For example, we can clean up our `greet()` method by extracting a `name` getter:\n\n```js\n// src/controllers/hello_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"name\" ]\n\n  greet() {\n    console.log(`Hello, ${this.name}!`)\n  }\n\n  get name() {\n    return this.nameTarget.value\n  }\n}\n```\n\n## Wrap-Up and Next Steps\n\nCongratulations—you've just written your first Stimulus controller!\n\nWe've covered the framework's most important concepts: controllers, actions, and targets. In the next chapter, we'll see how to put those together to build a real-life controller taken right from Basecamp.\n"
  },
  {
    "path": "docs/handbook/03_building_something_real.md",
    "content": "---\npermalink: /handbook/building-something-real.html\norder: 03\n---\n\n# Building Something Real\n\nWe've implemented our first controller and learned how Stimulus connects HTML to JavaScript. Now let's take a look at something we can use in a real application by recreating a controller from Basecamp.\n\n## Wrapping the DOM Clipboard API\n\nScattered throughout Basecamp's UI are buttons like these:\n\n<img src=\"../../assets/bc3-clipboard-ui.png\" width=\"1023\" height=\"317\" class=\"docs__screenshot\" alt=\"Screenshot showing a text field with an email address inside and a ”Copy to clipboard“ button to the right\">\n\nWhen you click one of these buttons, Basecamp copies a bit of text, such as a URL or an email address, to your clipboard.\n\nThe web platform has [an API for accessing the system clipboard](https://www.w3.org/TR/clipboard-apis/), but there's no HTML element that does what we need. To implement a \"Copy to clipboard\" button, we must use JavaScript.\n\n## Implementing a Copy Button\n\nLet's say we have an app which allows us to grant someone else access by generating a PIN for them. It would be convenient if we could display that generated PIN alongside a button to copy it to the clipboard for easy sharing.\n\nOpen `public/index.html` and replace the contents of `<body>` with a rough sketch of the button:\n\n```html\n<div>\n  PIN: <input type=\"text\" value=\"1234\" readonly>\n  <button>Copy to Clipboard</button>\n</div>\n```\n\n## Setting Up the Controller\n\nNext, create `src/controllers/clipboard_controller.js` and add an empty method `copy()`:\n\n```js\n// src/controllers/clipboard_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  copy() {\n  }\n}\n```\n\nThen add `data-controller=\"clipboard\"` to the outer `<div>`. Any time this attribute appears on an element, Stimulus will connect an instance of our controller:\n\n```html\n<div data-controller=\"clipboard\">\n```\n\n## Defining the Target\n\nWe'll need a reference to the text field so we can select its contents before invoking the clipboard API. Add `data-clipboard-target=\"source\"` to the text field:\n\n```html\n  PIN: <input data-clipboard-target=\"source\" type=\"text\" value=\"1234\" readonly>\n```\n\nNow add a target definition to the controller so we can access the text field element as `this.sourceTarget`:\n\n```js\nexport default class extends Controller {\n  static targets = [ \"source\" ]\n\n  // ...\n}\n```\n\n> ### What's With That `static targets` Line?\n>\n> When Stimulus loads your controller class, it looks for target name strings in a static array called `targets`. For each target name in the array, Stimulus adds three new properties to your controller. Here, our `\"source\"` target name becomes the following properties:\n>\n> * `this.sourceTarget` evaluates to the first `source` target in your controller's scope. If there is no `source` target, accessing the property throws an error.\n> * `this.sourceTargets` evaluates to an array of all `source` targets in the controller's scope.\n> * `this.hasSourceTarget` evaluates to `true` if there is a `source` target or `false` if not.\n>\n> You can read more about targets in the [reference documentation](/reference/targets).\n\n## Connecting the Action\n\nNow we're ready to hook up the Copy button.\n\nWe want a click on the button to invoke the `copy()` method in our controller, so we'll add `data-action=\"clipboard#copy\"`:\n\n```html\n  <button data-action=\"clipboard#copy\">Copy to Clipboard</button>\n```\n\n> ### Common Events Have a Shorthand Action Notation\n>\n> You might have noticed we've omitted `click->` from the action descriptor. That's because Stimulus defines `click` as the default event for actions on `<button>` elements.\n>\n> Certain other elements have default events, too. Here's the full list:\n>\n> | Element           | Default Event |\n> | ----------------- | ------------- |\n> | a                 | click         |\n> | button            | click         |\n> | details           | toggle        |\n> | form              | submit        |\n> | input             | input         |\n> | input type=submit | click         |\n> | select            | change        |\n> | textarea          | input         |\n\nFinally, in our `copy()` method, we can select the input field's contents and call the clipboard API:\n\n```js\n  copy() {\n    navigator.clipboard.writeText(this.sourceTarget.value)\n  }\n```\n\nLoad the page in your browser and click the Copy button. Then switch back to your text editor and paste. You should see the PIN `1234`.\n\n## Stimulus Controllers are Reusable\n\nSo far we've seen what happens when there's one instance of a controller on the page at a time.\n\nIt's not unusual to have multiple instances of a controller on the page simultaneously. For example, we might want to display a list of PINs, each with its own Copy button.\n\nOur controller is reusable: any time we want to provide a way to copy a bit of text to the clipboard, all we need is markup on the page with the right annotations.\n\nLet's go ahead and add another PIN to the page. Copy and paste the `<div>` so there are two identical PIN fields, then change the `value` attribute of the second:\n\n```html\n<div data-controller=\"clipboard\">\n  PIN: <input data-clipboard-target=\"source\" type=\"text\" value=\"3737\" readonly>\n  <button data-action=\"clipboard#copy\">Copy to Clipboard</button>\n</div>\n```\n\nReload the page and confirm that both buttons work.\n\n## Actions and Targets Can Go on Any Kind of Element\n\nNow let's add one more PIN field. This time we'll use a Copy _link_ instead of a button:\n\n```html\n<div data-controller=\"clipboard\">\n  PIN: <input data-clipboard-target=\"source\" type=\"text\" value=\"3737\" readonly>\n  <a href=\"#\" data-action=\"clipboard#copy\">Copy to Clipboard</a>\n</div>\n```\n\nStimulus lets us use any kind of element we want as long as it has an appropriate `data-action` attribute, and is a child of the controller component.\n\nNote that in this case, clicking the link will also cause the browser to follow the link's `href`. We can cancel this default behavior by calling `event.preventDefault()` in the action:\n\n```js\n  copy(event) {\n    event.preventDefault()\n    navigator.clipboard.writeText(this.sourceTarget.value)\n  }\n```\n\nSimilarly, our `source` target need not be an `<input type=\"text\">`. The controller only expects it to have a `value` property and a `copy()` method. That means we can use a `<textarea>` instead:\n\n```html\n  PIN: <textarea data-clipboard-target=\"source\" readonly>3737</textarea>\n```\n\n## Wrap-Up and Next Steps\n\nIn this chapter we looked at a real-life example of wrapping a browser API in a Stimulus controller. We saw how multiple instances of the controller can appear on the page at once, and we explored how actions and targets keep your HTML and JavaScript loosely coupled.\n\nNow let's see how small changes to the controller's design can lead us to a more robust implementation.\n"
  },
  {
    "path": "docs/handbook/04_designing_for_resilience.md",
    "content": "---\npermalink: /handbook/designing-for-resilience.html\norder: 04\n---\n\n# Designing For Resilience\n\nAlthough the clipboard API is [well-supported in current browsers](https://caniuse.com/#feat=clipboard), we might still expect to have a small number of people with older browsers using our application.\n\nWe should also expect people to have problems accessing our application from time to time. For example, intermittent network connectivity or CDN availability could prevent some or all of our JavaScript from loading.\n\nIt's tempting to write off support for older browsers as not worth the effort, or to dismiss network issues as temporary glitches that resolve themselves after a refresh. But often it's trivially easy to build features in a way that's gracefully resilient to these types of problems.\n\nThis resilient approach, commonly known as _progressive enhancement_, is the practice of delivering web interfaces such that the basic functionality is implemented in HTML and CSS, and tiered upgrades to that base experience are layered on top with CSS and JavaScript, progressively, when their underlying technologies are supported by the browser.\n\n## Progressively Enhancing the PIN Field\n\nLet's look at how we can progressively enhance our PIN field so that the Copy button is invisible unless it's supported by the browser. That way we can avoid showing someone a button that doesn't work.\n\nWe'll start by hiding the Copy button in CSS. Then we'll _feature-test_ support for the Clipboard API in our Stimulus controller. If the API is supported, we'll add a class name to the controller element to reveal the button.\n\nWe start off by adding `data-clipboard-supported-class=\"clipboard--supported\"` to the `div` element that has the `data-controller` attribute:\n\n```html\n  <div data-controller=\"clipboard\" data-clipboard-supported-class=\"clipboard--supported\">\n```\n\nThen add `class=\"clipboard-button\"` to the button element:\n\n```html\n  <button data-action=\"clipboard#copy\" class=\"clipboard-button\">Copy to Clipboard</button>\n```\n\nThen add the following styles to `public/main.css`:\n\n```css\n.clipboard-button {\n  display: none;\n}\n\n.clipboard--supported .clipboard-button {\n  display: initial;\n}\n```\n\nFirst we'll add the `data-clipboard-supported-class` attribute inside the controller as a static class:\n\n```js\n  static classes = [ \"supported\" ]\n```\n\nThis will let us control the specific CSS class in the HTML, so our controller becomes even more easily adaptable to different CSS approaches. The specific class added like this can be accessed via `this.supportedClass`.\n\nNow add a `connect()` method to the controller which will test to see if the clipboard API is supported and add a class name to the controller's element:\n\n```js\n  connect() {\n    if (\"clipboard\" in navigator) {\n      this.element.classList.add(this.supportedClass);\n    }\n  }\n```\n\nYou can place this method anywhere in the controller's class body.\n\nIf you wish, disable JavaScript in your browser, reload the page, and notice the Copy button is no longer visible.\n\nWe have progressively enhanced the PIN field: its Copy button's baseline state is hidden, becoming visible only when our JavaScript detects support for the clipboard API.\n\n## Wrap-Up and Next Steps\n\nIn this chapter we gently modified our clipboard controller to be resilient against older browsers and degraded network conditions.\n\nNext, we'll learn about how Stimulus controllers manage state.\n"
  },
  {
    "path": "docs/handbook/05_managing_state.md",
    "content": "---\npermalink: /handbook/managing-state.html\norder: 05\n---\n\n# Managing State\n\nMost contemporary frameworks encourage you to keep state in JavaScript at all times. They treat the DOM as a write-only rendering target, reconciled by client-side templates consuming JSON from the server.\n\nStimulus takes a different approach. A Stimulus application's state lives as attributes in the DOM; controllers themselves are largely stateless. This approach makes it possible to work with HTML from anywhere—the initial document, an Ajax request, a Turbo visit, or even another JavaScript library—and have associated controllers spring to life automatically without any explicit initialization step.\n\n## Building a Slideshow\n\nIn the last chapter, we learned how a Stimulus controller can maintain simple state in the document by adding a class name to an element. But what do we do when we need to store a value, not just a simple flag?\n\nWe'll investigate this question by building a slideshow controller which keeps its currently selected slide index in an attribute.\n\nAs usual, we'll begin with HTML:\n\n```html\n<div data-controller=\"slideshow\">\n  <button data-action=\"slideshow#previous\"> ← </button>\n  <button data-action=\"slideshow#next\"> → </button>\n\n  <div data-slideshow-target=\"slide\">🐵</div>\n  <div data-slideshow-target=\"slide\">🙈</div>\n  <div data-slideshow-target=\"slide\">🙉</div>\n  <div data-slideshow-target=\"slide\">🙊</div>\n</div>\n```\n\nEach `slide` target represents a single slide in the slideshow. Our controller will be responsible for making sure only one slide is visible at a time.\n\nLet's draft our controller. Create a new file, `src/controllers/slideshow_controller.js`, as follows:\n\n```js\n// src/controllers/slideshow_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"slide\" ]\n\n  initialize() {\n    this.index = 0\n    this.showCurrentSlide()\n  }\n\n  next() {\n    this.index++\n    this.showCurrentSlide()\n  }\n\n  previous() {\n    this.index--\n    this.showCurrentSlide()\n  }\n\n  showCurrentSlide() {\n    this.slideTargets.forEach((element, index) => {\n      element.hidden = index !== this.index\n    })\n  }\n}\n```\n\nOur controller defines a method, `showCurrentSlide()`, which loops over each slide target, toggling the [`hidden` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden) if its index matches.\n\nWe initialize the controller by showing the first slide, and the `next()` and `previous()` action methods advance and rewind the current slide.\n\n> ### Lifecycle Callbacks Explained\n>\n> What does the `initialize()` method do? How is it different from the `connect()` method we've used before?\n>\n> These are Stimulus _lifecycle callback_ methods, and they're useful for setting up or tearing down associated state when your controller enters or leaves the document.\n>\n> Method       | Invoked by Stimulus…\n> ------------ | --------------------\n> initialize() | Once, when the controller is first instantiated\n> connect()    | Anytime the controller is connected to the DOM\n> disconnect() | Anytime the controller is disconnected from the DOM\n\nReload the page and confirm that the Next button advances to the next slide.\n\n## Reading Initial State from the DOM\n\nNotice how our controller tracks its state—the currently selected slide—in the `this.index` property.\n\nNow say we'd like to start one of our slideshows with the second slide visible instead of the first. How can we encode the start index in our markup?\n\nOne way might be to load the initial index with an HTML `data` attribute. For example, we could add a `data-index` attribute to the controller's element:\n\n```html\n<div data-controller=\"slideshow\" data-index=\"1\">\n```\n\nThen, in our `initialize()` method, we could read that attribute, convert it to an integer, and pass it to `showCurrentSlide()`:\n\n```js\n  initialize() {\n    this.index = Number(this.element.dataset.index)\n    this.showCurrentSlide()\n  }\n```\n\nThis might get the job done, but it's clunky, requires us to make a decision about what to name the attribute, and doesn't help us if we want to access the index again or increment it and persist the result in the DOM.\n\n### Using Values\n\nStimulus controllers support typed value properties which automatically map to data attributes. When we add a value definition to the top of our controller class:\n\n```js\n  static values = { index: Number }\n```\n\nStimulus will create a `this.indexValue` controller property associated with a `data-slideshow-index-value` attribute, and handle the numeric conversion for us.\n\nLet's see that in action. Add the associated data attribute to our HTML:\n\n```html\n<div data-controller=\"slideshow\" data-slideshow-index-value=\"1\">\n```\n\nThen add a `static values` definition to the controller and change the `initialize()` method to log `this.indexValue`:\n\n```js\nexport default class extends Controller {\n  static values = { index: Number }\n\n  initialize() {\n    console.log(this.indexValue)\n    console.log(typeof this.indexValue)\n  }\n\n  // …\n}\n```\n\nReload the page and verify that the console shows `1` and `Number`.\n\n> ### What's with that `static values` line?\n>\n> Similar to targets, you define values in a Stimulus controller by describing them in a static object property called `values`. In this case, we've defined a single numeric value called `index`. You can read more about value definitions in the [reference documentation](/reference/values).\n\nNow let's update `initialize()` and the other methods in the controller to use `this.indexValue` instead of `this.index`. Here's how the controller should look when we're done:\n\n```js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"slide\" ]\n  static values = { index: Number }\n\n  initialize() {\n    this.showCurrentSlide()\n  }\n\n  next() {\n    this.indexValue++\n    this.showCurrentSlide()\n  }\n\n  previous() {\n    this.indexValue--\n    this.showCurrentSlide()\n  }\n\n  showCurrentSlide() {\n    this.slideTargets.forEach((element, index) => {\n      element.hidden = index !== this.indexValue\n    })\n  }\n}\n```\n\nReload the page and use the web inspector to confirm the controller element's `data-slideshow-index-value` attribute changes as you move from one slide to the next.\n\n### Change Callbacks\n\nOur revised controller improves on the original version, but the repeated calls to `this.showCurrentSlide()` stand out. We have to manually update the state of the document when the controller initializes and after every place where we update `this.indexValue`.\n\nWe can define a Stimulus value change callback to clean up the repetition and specify how the controller should respond whenever the index value changes.\n\nFirst, remove the `initialize()` method and define a new method, `indexValueChanged()`. Then remove the calls to `this.showCurrentSlide()` from `next()` and `previous()`:\n\n```js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"slide\" ]\n  static values = { index: Number }\n\n  next() {\n    this.indexValue++\n  }\n\n  previous() {\n    this.indexValue--\n  }\n\n  indexValueChanged() {\n    this.showCurrentSlide()\n  }\n\n  showCurrentSlide() {\n    this.slideTargets.forEach((element, index) => {\n      element.hidden = index !== this.indexValue\n    })\n  }\n}\n```\n\nReload the page and confirm the slideshow behavior is the same.\n\nStimulus calls the `indexValueChanged()` method at initialization and in response to any change to the `data-slideshow-index-value` attribute. You can even fiddle with the attribute in the web inspector and the controller will change slides in response. Go ahead—try it out!\n\n### Setting Defaults\n\nIt's also possible to set a default values as part of the static definition. This is done like so:\n\n```js\n  static values = { index: { type: Number, default: 2 } }\n```\n\nThat would start the index at 2, if no `data-slideshow-index-value` attribute was defined on the controller element. If you had other values, you can mix and match what needs a default and what doesn't:\n\n```js\n  static values = { index: Number, effect: { type: String, default: \"kenburns\" } }\n```\n\n## Wrap-Up and Next Steps\n\nIn this chapter we've seen how to use the values to load and persist the current index of a slideshow controller.\n\nFrom a usability perspective, our controller is incomplete. The Previous button appears to do nothing when you are looking at the first slide. Internally, `indexValue` decrements from `0` to `-1`. Could we make the value wrap around to the _last_ slide index instead? (There's a similar problem with the Next button.)\n\nNext we'll look at how to keep track of external resources, such as timers and HTTP requests, in Stimulus controllers.\n"
  },
  {
    "path": "docs/handbook/06_working_with_external_resources.md",
    "content": "---\npermalink: /handbook/working-with-external-resources.html\norder: 06\n---\n\n# Working With External Resources\n\nIn the last chapter we learned how to load and persist a controller's internal state using values.\n\nSometimes our controllers need to track the state of external resources, where by _external_ we mean anything that isn't in the DOM or a part of Stimulus. For example, we may need to issue an HTTP request and respond as the request's state changes. Or we may want to start a timer and then stop it when the controller is no longer connected. In this chapter we'll see how to do both of those things.\n\n## Asynchronously Loading HTML\n\nLet's learn how to populate parts of a page asynchronously by loading and inserting remote fragments of HTML. We use this technique in Basecamp to keep our initial page loads fast, and to keep our views free of user-specific content so they can be cached more effectively.\n\nWe'll build a general-purpose content loader controller which populates its element with HTML fetched from the server. Then we'll use it to load a list of unread messages like you'd see in an email inbox.\n\nBegin by sketching the inbox in `public/index.html`:\n\n```html\n<div data-controller=\"content-loader\"\n     data-content-loader-url-value=\"/messages.html\"></div>\n```\n\nThen create a new `public/messages.html` file with some HTML for our message list:\n\n```html\n<ol>\n  <li>New Message: Stimulus Launch Party</li>\n  <li>Overdue: Finish Stimulus 1.0</li>\n</ol>\n```\n\n(In a real application you'd generate this HTML dynamically on the server, but for demonstration purposes we'll just use a static file.)\n\nNow we can implement our controller:\n\n```js\n// src/controllers/content_loader_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static values = { url: String }\n\n  connect() {\n    this.load()\n  }\n\n  load() {\n    fetch(this.urlValue)\n      .then(response => response.text())\n      .then(html => this.element.innerHTML = html)\n  }\n}\n```\n\nWhen the controller connects, we kick off a [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) request to the URL specified in the element's `data-content-loader-url-value` attribute. Then we load the returned HTML by assigning it to our element's `innerHTML` property.\n\nOpen the network tab in your browser's developer console and reload the page. You'll see a request representing the initial page load, followed by our controller's subsequent request to `messages.html`.\n\n## Refreshing Automatically With a Timer\n\nLet's improve our controller by changing it to periodically refresh the inbox so it's always up-to-date.\n\nWe'll use the `data-content-loader-refresh-interval-value` attribute to specify how often the controller should reload its contents, in milliseconds:\n\n```html\n<div data-controller=\"content-loader\"\n     data-content-loader-url-value=\"/messages.html\"\n     data-content-loader-refresh-interval-value=\"5000\"></div>\n```\n\nNow we can update the controller to check for the interval and, if present, start a refresh timer.\n\nAdd a `static values` definition to the controller, and define a new method `startRefreshing()`:\n\n```js\nexport default class extends Controller {\n  static values = { url: String, refreshInterval: Number }\n\n  startRefreshing() {\n    setInterval(() => {\n      this.load()\n    }, this.refreshIntervalValue)\n  }\n\n  // …\n}\n```\n\nThen update the `connect()` method to call `startRefreshing()` if an interval value is present:\n\n```js\n  connect() {\n    this.load()\n\n    if (this.hasRefreshIntervalValue) {\n      this.startRefreshing()\n    }\n  }\n```\n\nReload the page and observe a new request once every five seconds in the developer console. Then make a change to `public/messages.html` and wait for it to appear in the inbox.\n\n## Releasing Tracked Resources\n\nWe start our timer when the controller connects, but we never stop it. That means if our controller's element were to disappear, the controller would continue to issue HTTP requests in the background.\n\nWe can fix this issue by modifying our `startRefreshing()` method to keep a reference to the timer:\n\n```js\n  startRefreshing() {\n    this.refreshTimer = setInterval(() => {\n      this.load()\n    }, this.refreshIntervalValue)\n  }\n```\n\nThen we can add a corresponding `stopRefreshing()` method below to cancel the timer:\n\n```js\n  stopRefreshing() {\n    if (this.refreshTimer) {\n      clearInterval(this.refreshTimer)\n    }\n  }\n```\n\nFinally, to instruct Stimulus to cancel the timer when the controller disconnects, we'll add a `disconnect()` method:\n\n```js\n  disconnect() {\n    this.stopRefreshing()\n  }\n```\n\nNow we can be sure a content loader controller will only issue requests when it's connected to the DOM.\n\nLet's take a look at our final controller class:\n\n```js\n// src/controllers/content_loader_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static values = { url: String, refreshInterval: Number }\n\n  connect() {\n    this.load()\n\n    if (this.hasRefreshIntervalValue) {\n      this.startRefreshing()\n    }\n  }\n\n  disconnect() {\n    this.stopRefreshing()\n  }\n\n  load() {\n    fetch(this.urlValue)\n      .then(response => response.text())\n      .then(html => this.element.innerHTML = html)\n  }\n\n  startRefreshing() {\n    this.refreshTimer = setInterval(() => {\n      this.load()\n    }, this.refreshIntervalValue)\n  }\n\n  stopRefreshing() {\n    if (this.refreshTimer) {\n      clearInterval(this.refreshTimer)\n    }\n  }\n}\n```\n\n## Using action parameters\n\nIf we wanted to make the loader work with multiple different sources, we could do it using action parameters. Take this HTML:\n\n```html\n<div data-controller=\"content-loader\">\n  <a href=\"#\" data-content-loader-url-param=\"/messages.html\" data-action=\"content-loader#load\">Messages</a>\n  <a href=\"#\" data-content-loader-url-param=\"/comments.html\" data-action=\"content-loader#load\">Comments</a>\n</div>\n```\n\nThen we can use those parameters through the `load` action:\n\n```js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  load({ params }) {\n    fetch(params.url)\n      .then(response => response.text())\n      .then(html => this.element.innerHTML = html)\n  }\n}\n```\n\nWe could even destruct the params to just get the URL parameter:\n\n```js\n  load({ params: { url } }) {\n    fetch(url)\n      .then(response => response.text())\n      .then(html => this.element.innerHTML = html)\n  }\n```\n\n## Wrap-Up and Next Steps\n\nIn this chapter we've seen how to acquire and release external resources using Stimulus lifecycle callbacks.\n\nNext we'll see how to install and configure Stimulus in your own application.\n"
  },
  {
    "path": "docs/handbook/07_installing_stimulus.md",
    "content": "---\npermalink: /handbook/installing.html\norder: 07\n---\n\n# Installing Stimulus in Your Application\n\nTo install Stimulus in your application, add the [`@hotwired/stimulus` npm package](https://www.npmjs.com/package/@hotwired/stimulus) to your JavaScript bundle. Or, import [`stimulus.js`](https://unpkg.com/@hotwired/stimulus/dist/stimulus.js) in a `<script type=\"module\">` tag.\n\n## Using Stimulus for Rails\n\nIf you're using [Stimulus for Rails](https://github.com/hotwired/stimulus-rails/) together with an [import map](https://github.com/rails/importmap-rails), the integration will automatically load all controller files from `app/javascript/controllers`.\n\n### Controller Filenames Map to Identifiers\n\nName your controller files `[identifier]_controller.js`, where `identifier` corresponds to each controller's `data-controller` identifier in your HTML.\n\nStimulus for Rails conventionally separates multiple words in filenames using underscores. Each underscore in a controller's filename translates to a dash in its identifier.\n\nYou may also namespace your controllers using subfolders. Each forward slash in a namespaced controller file's path becomes two dashes in its identifier.\n\nIf you prefer, you may use dashes instead of underscores anywhere in a controller's filename. Stimulus treats them identically.\n\nIf your controller file is named… | its identifier will be…\n--------------------------------- | -----------------------\nclipboard_controller.js           | clipboard\ndate_picker_controller.js         | date-picker\nusers/list_item_controller.js     | users\\-\\-list-item\nlocal-time-controller.js          | local-time\n\n## Using Webpack Helpers\n\nIf you're using Webpack as your JavaScript bundler, you can use the [@hotwired/stimulus-webpack-helpers](https://www.npmjs.com/package/@hotwired/stimulus-webpack-helpers) package to get the same form of autoloading behavior as Stimulus for Rails. First add the package, then use it like this:\n\n```js\nimport { Application } from \"@hotwired/stimulus\"\nimport { definitionsFromContext } from \"@hotwired/stimulus-webpack-helpers\"\n\nwindow.Stimulus = Application.start()\nconst context = require.context(\"./controllers\", true, /\\.js$/)\nStimulus.load(definitionsFromContext(context))\n```\n\n## Using Other Build Systems\n\nStimulus works with other build systems too, but without support for controller autoloading. Instead, you must explicitly load and register controller files with your application instance:\n\n```js\n// src/application.js\nimport { Application } from \"@hotwired/stimulus\"\n\nimport HelloController from \"./controllers/hello_controller\"\nimport ClipboardController from \"./controllers/clipboard_controller\"\n\nwindow.Stimulus = Application.start()\nStimulus.register(\"hello\", HelloController)\nStimulus.register(\"clipboard\", ClipboardController)\n```\n\nIf you're using stimulus-rails with a builder like esbuild, you can use the `stimulus:manifest:update` Rake task and `./bin/rails generate stimulus [controller]` generator to keep a controller index file located at `app/javascript/controllers/index.js` automatically updated.\n\n## Using Without a Build System\n\nIf you prefer not to use a build system, you can load Stimulus in a `<script type=\"module\">` tag:\n\n```html\n<!doctype html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <script type=\"module\">\n    import { Application, Controller } from \"https://unpkg.com/@hotwired/stimulus/dist/stimulus.js\"\n    window.Stimulus = Application.start()\n\n    Stimulus.register(\"hello\", class extends Controller {\n      static targets = [ \"name\" ]\n\n      connect() {\n      }\n    })\n  </script>\n</head>\n<body>\n  <div data-controller=\"hello\">\n    <input data-hello-target=\"name\" type=\"text\">\n    …\n  </div>\n</body>\n</html>\n```\n\n## Overriding Attribute Defaults\nIn case Stimulus `data-*` attributes conflict with another library in your project, they can be overridden when creating the Stimulus `Application`.\n\n- `data-controller`\n- `data-action`\n- `data-target`\n\nThese core Stimulus attributes can be overridden (see: [schema.ts](https://github.com/hotwired/stimulus/blob/main/src/core/schema.ts)):\n\n```js\n// src/application.js\nimport { Application, defaultSchema } from \"@hotwired/stimulus\"\n\nconst customSchema = {\n  ...defaultSchema,\n  actionAttribute: 'data-stimulus-action'\n}\n\nwindow.Stimulus = Application.start(document.documentElement, customSchema);\n```\n\n## Error handling\n\nAll calls from Stimulus to your application's code are wrapped in a `try ... catch` block.\n\nIf your code throws an error, it will be caught by Stimulus and logged to the browser console, including extra detail such as the controller name and event or lifecycle function being called. If you use an error tracking system that defines `window.onerror`, Stimulus will also pass the error on to it.\n\nYou can override how Stimulus handles errors by defining `Application#handleError`:\n\n```js\n// src/application.js\nimport { Application } from \"@hotwired/stimulus\"\nwindow.Stimulus = Application.start()\n\nStimulus.handleError = (error, message, detail) => {\n  console.warn(message, detail)\n  ErrorTrackingSystem.captureException(error)\n}\n```\n\n## Debugging\n\nIf you've assigned your Stimulus application to `window.Stimulus`, you can turn on [debugging mode](https://github.com/hotwired/stimulus/pull/354) from the console with `Stimulus.debug = true`. You can also set this flag when you're configuring your application instance in the source code.\n\n\n## Browser Support\n\nStimulus supports all evergreen, self-updating desktop and mobile browsers out of the box. Stimulus 3+ does not support Internet Explorer 11 (but you can use Stimulus 2 with the @stimulus/polyfills for that).\n"
  },
  {
    "path": "docs/reference/actions.md",
    "content": "---\npermalink: /reference/actions.html\norder: 02\n---\n\n# Actions\n\n_Actions_ are how you handle DOM events in your controllers.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"click->gallery#next\">\n\n```html\n<div data-controller=\"gallery\">\n  <button data-action=\"click->gallery#next\">…</button>\n</div>\n```\n\n<meta data-controller=\"callout\" data-callout-text-value=\"next\">\n\n```js\n// controllers/gallery_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  next(event) {\n    // …\n  }\n}\n```\n\nAn action is a connection between:\n\n* a controller method\n* the controller's element\n* a DOM event listener\n\n## Descriptors\n\nThe `data-action` value `click->gallery#next` is called an _action descriptor_. In this descriptor:\n\n* `click` is the name of the DOM event to listen for\n* `gallery` is the controller identifier\n* `next` is the name of the method to invoke\n\n### Event Shorthand\n\nStimulus lets you shorten the action descriptors for some common element/event pairs, such as the button/click pair above, by omitting the event name:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"gallery#next\">\n\n```html\n<button data-action=\"gallery#next\">…</button>\n```\n\nThe full set of these shorthand pairs is as follows:\n\nElement           | Default Event\n----------------- | -------------\na                 | click\nbutton            | click\ndetails           | toggle\nform              | submit\ninput             | input\ninput type=submit | click\nselect            | change\ntextarea          | input\n\n\n## KeyboardEvent Filter\n\nThere may be cases where [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) Actions should only call the Controller method when certain keystrokes are used.\n\nYou can install an event listener that responds only to the `Escape` key by adding `.esc` to the event name of the action descriptor, as in the following example.\n\n```html\n<div data-controller=\"modal\"\n     data-action=\"keydown.esc->modal#close\" tabindex=\"0\">\n</div>\n```\n\nThis will only work if the event being fired is a keyboard event.\n\nThe correspondence between these filter and keys is shown below.\n\nFilter    | Key Name\n--------  | --------\nenter     | Enter\ntab       | Tab\nesc       | Escape\nspace     | \" \"\nup        | ArrowUp\ndown      | ArrowDown\nleft      | ArrowLeft\nright     | ArrowRight\nhome      | Home\nend       | End\npage_up   | PageUp\npage_down | PageDown\n[a-z]     | [a-z]\n[0-9]     | [0-9]\n\nIf you need to support other keys, you can customize the modifiers using a custom schema.\n\n```javascript\nimport { Application, defaultSchema } from \"@hotwired/stimulus\"\n\nconst customSchema = {\n  ...defaultSchema,\n  keyMappings: { ...defaultSchema.keyMappings, at: \"@\" },\n}\n\nconst app = Application.start(document.documentElement, customSchema)\n```\n\nIf you want to subscribe to a compound filter using a modifier key, you can write it like `ctrl+a`.\n\n```html\n<div data-action=\"keydown.ctrl+a->listbox#selectAll\" role=\"option\" tabindex=\"0\">...</div>\n```\n\nThe list of supported modifier keys is shown below.\n\n| Modifier | Notes              |\n| -------- | ------------------ |\n| `alt`    | `option` on MacOS  |\n| `ctrl`   |                    |\n| `meta`   | Command key on MacOS |\n| `shift`  |                    |\n\n### Global Events\n\nSometimes a controller needs to listen for events dispatched on the global `window` or `document` objects.\n\nYou can append `@window` or `@document` to the event name (along with any filter modifier) in an action descriptor to install the event listener on `window` or `document`, respectively, as in the following example:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"resize@window\">\n\n```html\n<div data-controller=\"gallery\"\n     data-action=\"resize@window->gallery#layout\">\n</div>\n```\n\n### Options\n\nYou can append one or more _action options_ to an action descriptor if you need to specify [DOM event listener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters).\n\n<meta data-controller=\"callout\" data-callout-text-value=\":!passive\">\n<meta data-controller=\"callout\" data-callout-text-value=\":capture\">\n\n```html\n<div data-controller=\"gallery\"\n     data-action=\"scroll->gallery#layout:!passive\">\n  <img data-action=\"click->gallery#open:capture\">\n```\n\nStimulus supports the following action options:\n\nAction option | DOM event listener option\n------------- | -------------------------\n`:capture`    | `{ capture: true }`\n`:once`       | `{ once: true }`\n`:passive`    | `{ passive: true }`\n`:!passive`   | `{ passive: false }`\n\nOn top of that, Stimulus also supports the following action options which are not natively supported by the DOM event listener options:\n\nCustom action option | Description\n-------------------- | -----------\n`:stop`              | calls `.stopPropagation()` on the event before invoking the method\n`:prevent`           | calls `.preventDefault()` on the event before invoking the method\n`:self`              | only invokes the method if the event was fired by the element itself\n\nYou can register your own action options with the `Application.registerActionOption` method.\n\nFor example, consider that a `<details>` element will dispatch a [toggle][]\nevent whenever it's toggled. A custom `:open` action option would help\nto route events whenever the element is toggled _open_:\n\n```javascript\nimport { Application } from \"@hotwired/stimulus\"\n\nconst application = Application.start()\n\napplication.registerActionOption(\"open\", ({ event }) => {\n  if (event.type == \"toggle\") {\n    return event.target.open == true\n  } else {\n    return true\n  }\n})\n```\n\nSimilarly, a custom `:!open` action option could route events whenever the\nelement is toggled _closed_. Declaring the action descriptor option with a `!`\nprefix will yield a `value` argument set to `false` in the callback:\n\n```javascript\nimport { Application } from \"@hotwired/stimulus\"\n\nconst application = Application.start()\n\napplication.registerActionOption(\"open\", ({ event, value }) => {\n  if (event.type == \"toggle\") {\n    return event.target.open == value\n  } else {\n    return true\n  }\n})\n```\n\nIn order to prevent the event from being routed to the controller action, the\n`registerActionOption` callback function must return `false`. Otherwise, to\nroute the event to the controller action, return `true`.\n\nThe callback accepts a single object argument with the following keys:\n\n| Name       | Description                                                                                           |\n| ---------- | ----------------------------------------------------------------------------------------------------- |\n| name       | String: The option's name (`\"open\"` in the example above)                                             |\n| value      | Boolean: The value of the option (`:open` would yield `true`, `:!open` would yield `false`)           |\n| event      | [Event][]: The event instance, including with the `params` action parameters on the submitter element |\n| element    | [Element]: The element where the action descriptor is declared                                        |\n| controller | The `Controller` instance which would receive the method call                                         |\n\n[toggle]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement/toggle_event\n[Event]: https://developer.mozilla.org/en-US/docs/web/api/event\n[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element\n\n## Event Objects\n\nAn _action method_ is the method in a controller which serves as an action's event listener.\n\nThe first argument to an action method is the DOM _event object_. You may want access to the event for a number of reasons, including:\n\n* to read the key code from a keyboard event\n* to read the coordinates of a mouse event\n* to read data from an input event\n* to read params from the action submitter element\n* to prevent the browser's default behavior for an event\n* to find out which element dispatched an event before it bubbled up to this action\n\nThe following basic properties are common to all events:\n\nEvent Property      | Value\n------------------- | -----\nevent.type          | The name of the event (e.g. `\"click\"`)\nevent.target        | The target that dispatched the event (i.e. the innermost element that was clicked)\nevent.currentTarget | The target on which the event listener is installed (either the element with the `data-action` attribute, or `document` or `window`)\nevent.params        | The action params passed by the action submitter element\n\n<br>The following event methods give you more control over how events are handled:\n\nEvent Method            | Result\n----------------------- | ------\nevent.preventDefault()  | Cancels the event's default behavior (e.g. following a link or submitting a form)\nevent.stopPropagation() | Stops the event before it bubbles up to other listeners on parent elements\n\n## Multiple Actions\n\nThe `data-action` attribute's value is a space-separated list of action descriptors.\n\nIt's common for any given element to have many actions. For example, the following input element calls a `field` controller's `highlight()` method when it gains focus, and a `search` controller's `update()` method every time the element's value changes:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"focus->field#highlight\">\n<meta data-controller=\"callout\" data-callout-text-value=\"input->search#update\">\n\n```html\n<input type=\"text\" data-action=\"focus->field#highlight input->search#update\">\n```\n\nWhen an element has more than one action for the same event, Stimulus invokes the actions from left to right in the order that their descriptors appear.\n\nThe action chain can be stopped at any point by calling `Event#stopImmediatePropagation()` within an action. Any additional actions to the right will be ignored:\n\n```javascript\nhighlight(event) {\n  event.stopImmediatePropagation()\n  // ...\n}\n```\n\n## Naming Conventions\n\nAlways use camelCase to specify action names, since they map directly to methods on your controller.\n\nAvoid action names that simply repeat the event's name, such as `click`, `onClick`, or `handleClick`:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"#click\" data-callout-type-value=\"avoid\">\n\n```html\n<button data-action=\"click->profile#click\">Don't</button>\n```\n\nInstead, name your action methods based on what will happen when they're called:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"#showDialog\" data-callout-type-value=\"prefer\">\n\n```html\n<button data-action=\"click->profile#showDialog\">Do</button>\n```\n\nThis will help you reason about the behavior of a block of HTML without having to look at the controller source.\n\n## Action Parameters\n\nActions can have parameters that are be passed from the submitter element. They follow the format of `data-[identifier]-[param-name]-param`. Parameters must be specified on the same element as the action they intend to be passed to is declared.\n\nAll parameters are automatically typecast to either a `Number`, `String`, `Object`, or `Boolean`, inferred by their value:\n\nData attribute                                  | Param                | Type\n----------------------------------------------- | -------------------- | --------\n`data-item-id-param=\"12345\"`                    | `12345`              | Number\n`data-item-url-param=\"/votes\"`                  | `\"/votes\"`           | String\n`data-item-payload-param='{\"value\":\"1234567\"}'` | `{ value: 1234567 }` | Object\n`data-item-active-param=\"true\"`                 | `true`               | Boolean\n\n\n<br>Consider this setup:\n\n```html\n<div data-controller=\"item spinner\">\n  <button data-action=\"item#upvote spinner#start\" \n    data-item-id-param=\"12345\" \n    data-item-url-param=\"/votes\"\n    data-item-payload-param='{\"value\":\"1234567\"}' \n    data-item-active-param=\"true\">…</button>\n</div>\n```\n\nIt will call both `ItemController#upvote` and `SpinnerController#start`, but only the former will have any parameters passed to it:\n\n```js\n// ItemController\nupvote(event) {\n  // { id: 12345, url: \"/votes\", active: true, payload: { value: 1234567 } }\n  console.log(event.params) \n}\n\n// SpinnerController\nstart(event) {\n  // {}\n  console.log(event.params) \n}\n```\n\nIf we don't need anything else from the event, we can destruct the params:\n\n```js\nupvote({ params }) {\n  // { id: 12345, url: \"/votes\", active: true, payload: { value: 1234567 } }\n  console.log(params) \n}\n```\n\nOr destruct only the params we need, in case multiple actions on the same controller share the same submitter element:\n\n```js\nupvote({ params: { id, url } }) {\n  console.log(id) // 12345\n  console.log(url) // \"/votes\"\n}\n```\n"
  },
  {
    "path": "docs/reference/controllers.md",
    "content": "---\npermalink: /reference/controllers.html\norder: 00\n---\n\n# Controllers\n\nA _controller_ is the basic organizational unit of a Stimulus application.\n\n```js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  // …\n}\n```\n\nControllers are instances of JavaScript classes that you define in your application. Each controller class inherits from the `Controller` base class exported by the `@hotwired/stimulus` module.\n\n## Properties\n\nEvery controller belongs to a Stimulus `Application` instance and is associated with an HTML element. Within a controller class, you can access the controller's:\n\n* application, via the `this.application` property\n* HTML element, via the `this.element` property\n* identifier, via the `this.identifier` property\n\n## Modules\n\nDefine your controller classes in JavaScript modules, one per file. Export each controller class as the module's default object, as in the example above.\n\nPlace these modules in the `controllers/` directory. Name the files `[identifier]_controller.js`, where `[identifier]` corresponds to each controller's identifier.\n\n## Identifiers\n\nAn _identifier_ is the name you use to reference a controller class in HTML.\n\nWhen you add a `data-controller` attribute to an element, Stimulus reads the identifier from the attribute's value and creates a new instance of the corresponding controller class.\n\nFor example, this element has a controller which is an instance of the class defined in `controllers/reference_controller.js`:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"reference\">\n\n```html\n<div data-controller=\"reference\"></div>\n```\n\nThe following is an example of how Stimulus will generate identifiers for controllers in its require context:\n\nIf your controller file is named… | its identifier will be…\n--------------------------------- | -----------------------\nclipboard_controller.js           | clipboard\ndate_picker_controller.js         | date-picker\nusers/list_item_controller.js     | users\\-\\-list-item\nlocal-time-controller.js          | local-time\n\n## Scopes\n\nWhen Stimulus connects a controller to an element, that element and all of its children make up the controller's _scope_.\n\nFor example, the `<div>` and `<h1>` below are part of the controller's scope, but the surrounding `<main>` element is not.\n\n```html\n<main>\n  <div data-controller=\"reference\">\n    <h1>Reference</h1>\n  </div>\n</main>\n```\n\n## Nested Scopes\n\nWhen nested, each controller is only aware of its own scope excluding the scope of any controllers nested within.\n\nFor example, the `#parent` controller below is only aware of the `item` targets directly within its scope, but not any targets of the `#child` controller.\n\n```html\n<ul id=\"parent\" data-controller=\"list\">\n  <li data-list-target=\"item\">One</li>\n  <li data-list-target=\"item\">Two</li>\n  <li>\n    <ul id=\"child\" data-controller=\"list\">\n      <li data-list-target=\"item\">I am</li>\n      <li data-list-target=\"item\">a nested list</li>\n    </ul>\n  </li>\n</ul>\n```\n\n## Multiple Controllers\n\nThe `data-controller` attribute's value is a space-separated list of identifiers:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"clipboard\">\n<meta data-controller=\"callout\" data-callout-text-value=\"list-item\">\n\n```html\n<div data-controller=\"clipboard list-item\"></div>\n```\n\nIt's common for any given element on the page to have many controllers. In the example above, the `<div>` has two connected controllers, `clipboard` and `list-item`.\n\nSimilarly, it's common for multiple elements on the page to reference the same controller class:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"list-item\">\n\n```html\n<ul>\n  <li data-controller=\"list-item\">One</li>\n  <li data-controller=\"list-item\">Two</li>\n  <li data-controller=\"list-item\">Three</li>\n</ul>\n```\n\nHere, each `<li>` has its own instance of the `list-item` controller.\n\n## Naming Conventions\n\nAlways use camelCase for method and property names in a controller class.\n\nWhen an identifier is composed of more than one word, write the words in kebab-case (i.e., by using dashes: `date-picker`, `list-item`).\n\nIn filenames, separate multiple words using either underscores or dashes (snake_case or kebab-case: `controllers/date_picker_controller.js`, `controllers/list-item-controller.js`).\n\n## Registration\n\nIf you use Stimulus for Rails with an import map or Webpack together with the `@hotwired/stimulus-webpack-helpers` package, your application will automatically load and register controller classes following the conventions above.\n\nIf not, your application must manually load and register each controller class.\n\n### Registering Controllers Manually\n\nTo manually register a controller class with an identifier, first import the class, then call the `Application#register` method on your application object:\n\n```js\nimport ReferenceController from \"./controllers/reference_controller\"\n\napplication.register(\"reference\", ReferenceController)\n```\n\nYou can also register a controller class inline instead of importing it from a module:\n\n```js\nimport { Controller } from \"@hotwired/stimulus\"\n\napplication.register(\"reference\", class extends Controller {\n  // …\n})\n```\n\n### Preventing Registration Based On Environmental Factors\n\nIf you only want a controller registered and loaded if certain environmental factors are met – such a given user agent – you can overwrite the static `shouldLoad` method:\n\n```js\nclass UnloadableController extends ApplicationController {\n  static get shouldLoad() {\n    return false\n  }\n}\n\n// This controller will not be loaded\napplication.register(\"unloadable\", UnloadableController)\n```\n\n### Trigger Behaviour When A Controller Is Registered\n\nIf you want to trigger some behaviour once a controller has been registered you can add a static `afterLoad` method:\n\n```js\nclass SpinnerButton extends Controller {\n  static legacySelector = \".legacy-spinner-button\"\n\n  static afterLoad(identifier, application) {\n    // use the application instance to read the configured 'data-controller' attribute\n    const { controllerAttribute } = application.schema\n\n    // update any legacy buttons with the controller's registered identifier\n    const updateLegacySpinners = () => {\n      document.querySelector(this.legacySelector).forEach((element) => {\n        element.setAttribute(controllerAttribute, identifier)\n      })\n    }\n\n    // called as soon as registered so DOM may not have loaded yet\n    if (document.readyState == \"loading\") {\n      document.addEventListener(\"DOMContentLoaded\", updateLegacySpinners)\n    } else {\n      updateLegacySpinners()\n    }\n  }\n}\n\n// This controller will update any legacy spinner buttons to use the controller\napplication.register(\"spinner-button\", SpinnerButton)\n```\n\nThe `afterLoad` method will get called as soon as the controller has been registered, even if no controlled elements exist in the DOM. The function will be called bound to the original controller constructor along with two arguments; the `identifier` that was used when registering the controller and the Stimulus application instance.\n\n## Cross-Controller Coordination With Events\n\nIf you need controllers to communicate with each other, you should use events. The `Controller` class has a convenience method called `dispatch` that makes this easier. It takes an `eventName` as the first argument, which is then automatically prefixed with the name of the controller separated by a colon. The payload is held in `detail`. It works like this:\n\n```js\nclass ClipboardController extends Controller {\n  static targets = [ \"source\" ]\n\n  copy() {\n    this.dispatch(\"copy\", { detail: { content: this.sourceTarget.value } })\n    navigator.clipboard.writeText(this.sourceTarget.value)\n  }\n}\n```\n\nAnd this event can then be routed to an action on another controller:\n\n```html\n<div data-controller=\"clipboard effects\" data-action=\"clipboard:copy->effects#flash\">\n  PIN: <input data-clipboard-target=\"source\" type=\"text\" value=\"1234\" readonly>\n  <button data-action=\"clipboard#copy\">Copy to Clipboard</button>\n</div>\n```\n\nSo when the `Clipboard#copy` action is invoked, the `Effects#flash` action will be too:\n\n```js\nclass EffectsController extends Controller {\n  flash({ detail: { content } }) {\n    console.log(content) // 1234\n  }\n}\n```\n\nIf the two controllers don't belong to the same HTML element, the `data-action` attribute\nneeds to be added to the *receiving* controller's element. And if the receiving controller's\nelement is not a parent (or same) of the emitting controller's element, you need to add\n`@window` to the event:\n\n```html\n<div data-action=\"clipboard:copy@window->effects#flash\">\n```\n\n`dispatch` accepts additional options as the second parameter as follows:\n\noption       | default            | notes\n-------------|--------------------|----------------------------------------------------------------------------------------------\n`detail`     | `{}` empty object  | See [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail)\n`target`     | `this.element`     | See [Event.target](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)\n`prefix`     | `this.identifier`  | If the prefix is falsey (e.g. `null` or `false`), only the `eventName` will be used. If you provide a string value the `eventName` will be prepended with the provided string and a colon. \n`bubbles`    | `true`             | See [Event.bubbles](https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles)\n`cancelable` | `true`             | See [Event.cancelable](https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable)\n\n`dispatch` will return the generated [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent), you can use this to provide a way for the event to be cancelled by any other listeners as follows:\n\n```js\nclass ClipboardController extends Controller {\n  static targets = [ \"source\" ]\n\n  copy() {\n    const event = this.dispatch(\"copy\", { cancelable: true })\n    if (event.defaultPrevented) return\n    navigator.clipboard.writeText(this.sourceTarget.value)\n  }\n}\n```\n\n```js\nclass EffectsController extends Controller {\n  flash(event) {\n    // this will prevent the default behaviour as determined by the dispatched event\n    event.preventDefault()\n  }\n}\n```\n\n## Directly Invoking Other Controllers\n\nIf for some reason it is not possible to use events to communicate between controllers, you can reach a controller instance via the `getControllerForElementAndIdentifier` method from the application. This should only be used if you have a unique problem that cannot be solved through the more general way of using events, but if you must, this is how:\n\n```js\nclass MyController extends Controller {\n  static targets = [ \"other\" ]\n\n  copy() {\n    const otherController = this.application.getControllerForElementAndIdentifier(this.otherTarget, 'other')\n    otherController.otherMethod()\n  }\n}\n```\n"
  },
  {
    "path": "docs/reference/css_classes.md",
    "content": "---\npermalink: /reference/css-classes.html\norder: 06\n---\n\n# CSS Classes\n\nIn HTML, a _CSS class_ defines a set of styles which can be applied to elements using the `class` attribute.\n\nCSS classes are a convenient tool for changing styles and playing animations programmatically. For example, a Stimulus controller might add a \"loading\" class to an element when it is performing an operation in the background, and then style that class in CSS to display a progress indicator:\n\n```html\n<form data-controller=\"search\" class=\"search--busy\">\n```\n\n```css\n.search--busy {\n  background-image: url(throbber.svg) no-repeat;\n}\n```\n\nAs an alternative to hard-coding classes with JavaScript strings, Stimulus lets you refer to CSS classes by _logical name_ using a combination of data attributes and controller properties.\n\n## Definitions\n\nDefine CSS classes by logical name in your controller using the `static classes` array:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"static classes = [ &quot;loading&quot; ]\">\n\n```js\n// controllers/search_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static classes = [ \"loading\" ]\n\n  // …\n}\n```\n\n## Attributes\n\nThe logical names defined in the controller's `static classes` array map to _CSS class attributes_ on the controller's element.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"data-search-loading-class=&quot;search--busy&quot;\">\n\n```html\n<form data-controller=\"search\"\n      data-search-loading-class=\"search--busy\">\n  <input data-action=\"search#loadResults\">\n</form>\n```\n\nConstruct a CSS class attribute by joining together the controller identifier and logical name in the format `data-[identifier]-[logical-name]-class`. The attribute's value can be a single CSS class name or a list of multiple class names.\n\n**Note:** CSS class attributes must be specified on the same element as the `data-controller` attribute.\n\nIf you want to specify multiple CSS classes for a logical name, separate the classes with spaces:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"data-search-loading-class=&quot;bg-gray-500 animate-spinner cursor-busy&quot;\">\n\n```html\n<form data-controller=\"search\"\n      data-search-loading-class=\"bg-gray-500 animate-spinner cursor-busy\">\n  <input data-action=\"search#loadResults\">\n</form>\n```\n\n## Properties\n\nFor each logical name defined in the `static classes` array, Stimulus adds the following _CSS class properties_ to your controller:\n\nKind        | Name                         | Value\n----------- | ---------------------------- | -----\nSingular    | `this.[logicalName]Class`    | The value of the CSS class attribute corresponding to `logicalName`\nPlural      | `this.[logicalName]Classes`  | An array of all classes in the corresponding CSS class attribute, split by spaces\nExistential | `this.has[LogicalName]Class` | A boolean indicating whether or not the CSS class attribute is present\n\n<br>Use these properties to apply CSS classes to elements with the `add()` and `remove()` methods of the [DOM `classList` API](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).\n\nFor example, to display a loading indicator on the `search` controller's element before fetching results, you might implement the `loadResults` action like so:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"this.loadingClass\">\n\n```js\nexport default class extends Controller {\n  static classes = [ \"loading\" ]\n\n  loadResults() {\n    this.element.classList.add(this.loadingClass)\n\n    fetch(/* … */)\n  }\n}\n```\n\nIf a CSS class attribute contains a list of class names, its singular CSS class property returns the first class in the list.\n\nUse the plural CSS class property to access all class names as an array. Combine this with [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) to apply multiple classes at once:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"...this.loadingClasses\">\n\n```js\nexport default class extends Controller {\n  static classes = [ \"loading\" ]\n\n  loadResults() {\n    this.element.classList.add(...this.loadingClasses)\n\n    fetch(/* … */)\n  }\n}\n```\n\n**Note:** Stimulus will throw an error if you attempt to access a CSS class property when a matching CSS class attribute is not present.\n\n## Naming Conventions\n\nUse camelCase to specify logical names in CSS class definitions. Logical names map to camelCase CSS class properties:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"noResultsClass\">\n<meta data-controller=\"callout\" data-callout-text-value=\"noResults\">\n\n```js\nexport default class extends Controller {\n  static classes = [ \"loading\", \"noResults\" ]\n\n  loadResults() {\n    // …\n    if (results.length == 0) {\n      this.element.classList.add(this.noResultsClass)\n    }\n  }\n}\n```\n\nIn HTML, write CSS class attributes in kebab-case:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"no-results\">\n\n```html\n<form data-controller=\"search\"\n      data-search-loading-class=\"search--busy\"\n      data-search-no-results-class=\"search--empty\">\n```\n\nWhen constructing CSS class attributes, follow the conventions for identifiers as described in [Controllers: Naming Conventions](controllers#naming-conventions).\n"
  },
  {
    "path": "docs/reference/lifecycle_callbacks.md",
    "content": "---\npermalink: /reference/lifecycle-callbacks.html\norder: 01\n---\n\n# Lifecycle Callbacks\n\nSpecial methods called _lifecycle callbacks_ allow you to respond whenever a controller or certain targets connects to and disconnects from the document.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"connect()\">\n\n```js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  connect() {\n    // …\n  }\n}\n```\n\n## Methods\n\nYou may define any of the following methods in your controller:\n\nMethod       | Invoked by Stimulus…\n------------ | --------------------\ninitialize() | Once, when the controller is first instantiated\n[name]TargetConnected(target: Element) | Anytime a target is connected to the DOM\nconnect()    | Anytime the controller is connected to the DOM\n[name]TargetDisconnected(target: Element) | Anytime a target is disconnected from the DOM\ndisconnect() | Anytime the controller is disconnected from the DOM\n\n## Connection\n\nA controller is _connected_ to the document when both of the following conditions are true:\n\n* its element is present in the document (i.e., a descendant of `document.documentElement`, the `<html>` element)\n* its identifier is present in the element's `data-controller` attribute\n\nWhen a controller becomes connected, Stimulus calls its `connect()` method.\n\n### Targets\n\nA target is _connected_ to the document when both of the following conditions are true:\n\n* its element is present in the document as a descendant of its corresponding controller's element\n* its identifier is present in the element's `data-{identifier}-target` attribute\n\nWhen a target becomes connected, Stimulus calls its controller's `[name]TargetConnected()` method, passing the target element as a parameter. The `[name]TargetConnected()` lifecycle callbacks will fire *before* the controller's `connect()` callback.\n\n## Disconnection\n\nA connected controller will later become _disconnected_ when either of the preceding conditions becomes false, such as under any of the following scenarios:\n\n* the element is explicitly removed from the document with `Node#removeChild()` or `ChildNode#remove()`\n* one of the element's parent elements is removed from the document\n* one of the element's parent elements has its contents replaced by `Element#innerHTML=`\n* the element's `data-controller` attribute is removed or modified\n* the document installs a new `<body>` element, such as during a Turbo page change\n\nWhen a controller becomes disconnected, Stimulus calls its `disconnect()` method.\n\n### Targets\n\nA connected target will later become _disconnected_ when either of the preceding conditions becomes false, such as under any of the following scenarios:\n\n* the element is explicitly removed from the document with `Node#removeChild()` or `ChildNode#remove()`\n* one of the element's parent elements is removed from the document\n* one of the element's parent elements has its contents replaced by `Element#innerHTML=`\n* the element's `data-{identifier}-target` attribute is removed or modified\n* the document installs a new `<body>` element, such as during a Turbo page change\n\nWhen a target becomes disconnected, Stimulus calls its controller's `[name]TargetDisconnected()` method, passing the target element as a parameter. The `[name]TargetDisconnected()` lifecycle callbacks will fire *after* the controller's `disconnect()` callback.\n\n## Reconnection\n\nA disconnected controller may become connected again at a later time.\n\nWhen this happens, such as after removing the controller's element from the document and then re-attaching it, Stimulus will reuse the element's previous controller instance, calling its `connect()` method multiple times.\n\nSimilarly, a disconnected target may be connected again at a later time. Stimulus will invoke its controller's `[name]TargetConnected()` method multiple times.\n\n## Order and Timing\n\nStimulus watches the page for changes asynchronously using the [DOM `MutationObserver` API](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).\n\nThis means that Stimulus calls your controller's lifecycle methods asynchronously after changes are made to the document, in the next [microtask](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) following each change.\n\nLifecycle methods still run in the order they occur, so two calls to a controller's `connect()` method will always be separated by one call to `disconnect()`. Similarly, two calls to a controller's `[name]TargetConnected()` for a given target will always be separated by one call to `[name]TargetDisconnected()` for that same target.\n"
  },
  {
    "path": "docs/reference/outlets.md",
    "content": "---\npermalink: /reference/outlets.html\norder: 04\n---\n\n# Outlets\n\n_Outlets_ let you reference Stimulus _controller instances_ and their _controller element_ from within another Stimulus Controller by using CSS selectors.\n\nThe use of Outlets helps with cross-controller communication and coordination as an alternative to dispatching custom events on controller elements.\n\nThey are conceptually similar to [Stimulus Targets](https://stimulus.hotwired.dev/reference/targets) but with the difference that they reference a Stimulus controller instance plus its associated controller element.\n\n<meta data-controller=\"callout\" data-callout-text-value='data-chat-user-status-outlet=\".online-user\"'>\n<meta data-controller=\"callout\" data-callout-text-value='class=\"online-user\"'>\n\n\n```html\n<div>\n  <div class=\"online-user\" data-controller=\"user-status\">...</div>\n  <div class=\"online-user\" data-controller=\"user-status\">...</div>\n  ...\n</div>\n\n...\n\n<div data-controller=\"chat\" data-chat-user-status-outlet=\".online-user\">\n  ...\n</div>\n```\n\nWhile a **target** is a specifically marked element **within the scope** of its own controller element, an **outlet** can be located **anywhere on the page** and doesn't necessarily have to be within the controller scope.\n\n## Attributes and Names\n\nThe `data-chat-user-status-outlet` attribute is called an _outlet attribute_, and its value is a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) which you can use to refer to other controller elements which should be available as outlets on the _host controller_. The outlet identifier in the host controller **must be the same** as the target controller's identifier.  If not, it will throw an error message that outlet does not exist. \n\n```html\ndata-[identifier]-[outlet]-outlet=\"[selector]\"\n```\n\n<meta data-controller=\"callout\" data-callout-text-value='data-chat-user-status-outlet=\".online-user\"'>\n\n\n```html\n<div data-controller=\"chat\" data-chat-user-status-outlet=\".online-user\"></div>\n```\n\n## Definitions\n\nDefine controller identifiers in your controller class using the `static outlets` array. This array declares which other controller identifiers can be used as outlets on this controller:\n\n<meta data-controller=\"callout\" data-callout-text-value='static outlets'>\n<meta data-controller=\"callout\" data-callout-text-value='\"user-status\"'>\n<meta data-controller=\"callout\" data-callout-text-value='userStatus'>\n\n\n```js\n// chat_controller.js\n\nexport default class extends Controller {\n  static outlets = [ \"user-status\" ]\n\n  connect () {\n    this.userStatusOutlets.forEach(status => ...)\n  }\n}\n```\n\n## Properties\n\nFor each outlet defined in the `static outlets` array, Stimulus adds five properties to your controller, where `[name]` corresponds to the outlet's controller identifier:\n\n| Kind | Property name | Return Type | Effect\n| ---- | ------------- | ----------- | -----------\n| Existential | `has[Name]Outlet` | `Boolean` | Tests for presence of a `[name]` outlet\n| Singular | `[name]Outlet` | `Controller` | Returns the `Controller` instance of the first `[name]` outlet or throws an exception if none is present\n| Plural | `[name]Outlets` | `Array<Controller>` | Returns the `Controller` instances of all `[name]` outlets\n| Singular | `[name]OutletElement` | `Element` | Returns the Controller `Element` of the first `[name]` outlet or throws an exception if none is present\n| Plural | `[name]OutletElements` | `Array<Element>` | Returns the Controller `Element`'s of all `[name]` outlets\n\n**Note:** For nested Stimulus controller properties, make sure to omit namespace delimiters in order to correctly access the referenced outlet:\n\n```js\n// chat_controller.js\n\nexport default class extends Controller {\n  static outlets = [ \"admin--user-status\" ]\n\n  selectAll(event) {\n    // returns undefined\n    this.admin__UserStatusOutlets\n\n    // returns controller reference\n    this.adminUserStatusOutlets\n  }\n}\n```\n\n## Accessing Controllers and Elements\n\nSince you get back a `Controller` instance from the `[name]Outlet` and `[name]Outlets` properties you are also able to access the Values, Classes, Targets and all of the other properties and functions that controller instance defines:\n\n```js\nthis.userStatusOutlet.idValue\nthis.userStatusOutlet.imageTarget\nthis.userStatusOutlet.activeClasses\n```\n\nYou are also able to invoke any function the outlet controller may define:\n\n```js\n// user_status_controller.js\n\nexport default class extends Controller {\n  markAsSelected(event) {\n    // ...\n  }\n}\n\n// chat_controller.js\n\nexport default class extends Controller {\n  static outlets = [ \"user-status\" ]\n\n  selectAll(event) {\n    this.userStatusOutlets.forEach(status => status.markAsSelected(event))\n  }\n}\n```\n\nSimilarly with the Outlet Element, it allows you to call any function or property on [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element):\n\n```js\nthis.userStatusOutletElement.dataset.value\nthis.userStatusOutletElement.getAttribute(\"id\")\nthis.userStatusOutletElements.map(status => status.hasAttribute(\"selected\"))\n```\n\n## Outlet Callbacks\n\nOutlet callbacks are specially named functions called by Stimulus to let you respond to whenever an outlet is added or removed from the page.\n\nTo observe outlet changes, define a function named `[name]OutletConnected()` or `[name]OutletDisconnected()`.\n\n```js\n// chat_controller.js\n\nexport default class extends Controller {\n  static outlets = [ \"user-status\" ]\n\n  userStatusOutletConnected(outlet, element) {\n    // ...\n  }\n\n  userStatusOutletDisconnected(outlet, element) {\n    // ...\n  }\n}\n```\n\n### Outlets are Assumed to be Present\n\nWhen you access an Outlet property in a Controller, you assert that at least one corresponding Outlet is present. If the declaration is missing and no matching outlet is found Stimulus will throw an exception:\n\n```html\nMissing outlet element \"user-status\" for \"chat\" controller\n```\n\n### Optional outlets\n\nIf an Outlet is optional or you want to assert that at least Outlet is present, you must first check the presence of the Outlet using the existential property:\n\n```js\nif (this.hasUserStatusOutlet) {\n  this.userStatusOutlet.safelyCallSomethingOnTheOutlet()\n}\n```\n\n### Referencing Non-Controller Elements\n\nStimulus will throw an exception if you try to declare an element as an outlet which doesn't have a corresponding `data-controller` and identifier on it:\n\n<meta data-controller=\"callout\" data-callout-text-value='data-chat-user-status-outlet=\"#user-column\"'>\n<meta data-controller=\"callout\" data-callout-text-value='id=\"user-column\"'>\n\n\n```html\n<div data-controller=\"chat\" data-chat-user-status-outlet=\"#user-column\"></div>\n\n<div id=\"user-column\"></div>\n```\n\nWould result in:\n```html\nMissing \"data-controller=user-status\" attribute on outlet element for\n\"chat\" controller`\n```\n"
  },
  {
    "path": "docs/reference/targets.md",
    "content": "---\npermalink: /reference/targets.html\norder: 03\n---\n\n# Targets\n\n_Targets_ let you reference important elements by name.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"search.query\">\n<meta data-controller=\"callout\" data-callout-text-value=\"search.errorMessage\">\n<meta data-controller=\"callout\" data-callout-text-value=\"search.results\">\n\n```html\n<div data-controller=\"search\">\n  <input type=\"text\" data-search-target=\"query\">\n  <div data-search-target=\"errorMessage\"></div>\n  <div data-search-target=\"results\"></div>\n</div>\n```\n\n## Attributes and Names\n\nThe `data-search-target` attribute is called a _target attribute_, and its value is a space-separated list of _target names_ which you can use to refer to the element in the `search` controller.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"search\">\n<meta data-controller=\"callout\" data-callout-text-value=\"results\">\n\n```html\n<div data-controller=\"s​earch\">\n  <div data-search-target=\"results\"></div>\n</div>\n```\n\n## Definitions\n\nDefine target names in your controller class using the `static targets` array:\n\n```js\n// controllers/search_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"query\", \"errorMessage\", \"results\" ]\n  // …\n}\n```\n\n## Properties\n\nFor each target name defined in the `static targets` array, Stimulus adds the following properties to your controller, where `[name]` corresponds to the target's name:\n\nKind        | Name                   | Value\n----------- | ---------------------- | -----\nSingular    | `this.[name]Target`    | The first matching target in scope\nPlural      | `this.[name]Targets`   | An array of all matching targets in scope\nExistential | `this.has[Name]Target` | A boolean indicating whether there is a matching target in scope\n\n<br>**Note:** Accessing the singular target property will throw an error when there is no matching element.\n\n## Shared Targets\n\nElements can have more than one target attribute, and it's common for targets to be shared by multiple controllers.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"data-search-target=&quot;projects&quot;\">\n<meta data-controller=\"callout\" data-callout-text-value=\"data-search-target=&quot;messages&quot;\">\n<meta data-controller=\"callout\" data-callout-text-value=\"data-checkbox-target=&quot;input&quot;\">\n\n```html\n<form data-controller=\"search checkbox\">\n  <input type=\"checkbox\" data-search-target=\"projects\" data-checkbox-target=\"input\">\n  <input type=\"checkbox\" data-search-target=\"messages\" data-checkbox-target=\"input\">\n  …\n</form>\n```\n\nIn the example above, the checkboxes are accessible inside the `search` controller as `this.projectsTarget` and `this.messagesTarget`, respectively.\n\nInside the `checkbox` controller, `this.inputTargets` returns an array with both checkboxes.\n\n## Optional Targets\n\nIf your controller needs to work with a target which may or may not be present, condition your code based on the value of the existential target property:\n\n```js\nif (this.hasResultsTarget) {\n  this.resultsTarget.innerHTML = \"…\"\n}\n```\n\n## Connected and Disconnected Callbacks\n\nTarget _element callbacks_ let you respond whenever a target element is added or\nremoved within the controller's element.\n\nDefine a method `[name]TargetConnected` or `[name]TargetDisconnected` in the controller, where `[name]` is the name of the target you want to observe for additions or removals. The method receives the element as the first argument.\n\nStimulus invokes each element callback any time its target elements are added or removed. When the controller is connected or disconnected from the document, these callbacks are invoked *before* `connect()` and *after* `disconnect()` lifecycle hooks.\n\n```js\nexport default class extends Controller {\n  static targets = [ \"item\" ]\n\n  itemTargetConnected(element) {\n    this.sortElements(this.itemTargets)\n  }\n\n  itemTargetDisconnected(element) {\n    this.sortElements(this.itemTargets)\n  }\n\n  // Private\n  sortElements(itemTargets) { /* ... */ }\n}\n```\n\n**Note** During the execution of `[name]TargetConnected` and\n`[name]TargetDisconnected` callbacks, the `MutationObserver` instances behind\nthe scenes are paused. This means that if a callback add or removes a target\nwith a matching name, the corresponding callback _will not_ be invoked again.\n\n## Naming Conventions\n\nAlways use camelCase to specify target names, since they map directly to properties on your controller:\n\n```html\n<span data-search-target=\"camelCase\"></span>\n<span data-search-target=\"do-not-do-this\"></span>\n```\n\n```js\nexport default class extends Controller {\n  static targets = [ \"camelCase\" ]\n}\n```\n"
  },
  {
    "path": "docs/reference/using_typescript.md",
    "content": "---\npermalink: /reference/using-typescript.html\norder: 07\n---\n\n# Using Typescript\n\nStimulus itself is written in [TypeScript](https://www.typescriptlang.org/) and provides types directly over its package.\nThe following documentation shows how to define types for Stimulus properties.\n\n## Define Controller Element Type\n\nBy default, the `element` of the controller is of type `Element`. You can override the type of the controller element by specifiying it as a [Generic Type](https://www.typescriptlang.org/docs/handbook/2/generics.html). For example, if the element type is expected to be a `HTMLFormElement`:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"Controller<HTMLFormElement>\">\n\n```ts\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class MyController extends Controller<HTMLFormElement> {\n  submit() {\n    new FormData(this.element)\n  }\n}\n```\n\n## Define Value Properties\n\nYou can define the properties of configured values using the TypeScript `declare` keyword. You just need to define the properties if you are making use of them within the controller.\n\n```ts\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class MyController extends Controller {\n  static values = {\n    code: String\n  }\n\n  declare codeValue: string\n  declare readonly hasCodeValue: boolean\n}\n```\n\n> The `declare` keyword avoids overriding the existing Stimulus property, and just defines the type for TypeScript.\n\n## Define Target Properties\n\nYou can define the properties of configured targets using the TypeScript `declare` keyword. You just need to define the properties if you are making use of them within the controller.\n\n The return types of the `[name]Target` and `[name]Targets` properties can be any inheriting from the `Element` type. Choose the best type which fits your needs. Pick either `Element` or `HTMLElement` if you want to define it as a generic HTML element.\n\n```ts\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class MyController extends Controller {\n  static targets = [ \"input\" ]\n\n  declare readonly hasInputTarget: boolean\n  declare readonly inputTarget: HTMLInputElement\n  declare readonly inputTargets: HTMLInputElement[]\n}\n```\n\n> The `declare` keyword avoids overriding the existing Stimulus property, and just defines the type for TypeScript.\n\n## Custom properties and methods\n\nOther custom properties can be defined the TypeScript way on the controller class:\n\n<meta data-controller=\"callout\" data-callout-text-value=\"container: HTMLElement\">\n\n```ts\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class MyController extends Controller {\n  container: HTMLElement\n}\n```\n\nRead more in the [TypeScript Documentation](https://www.typescriptlang.org/docs/handbook/intro.html).\n"
  },
  {
    "path": "docs/reference/values.md",
    "content": "---\npermalink: /reference/values.html\norder: 05\n---\n\n# Values\n\nYou can read and write [HTML data attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) on controller elements as typed _values_ using special controller properties.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"data-loader-url-value=&quot;/messages&quot;\">\n\n```html\n<div data-controller=\"loader\" data-loader-url-value=\"/messages\">\n</div>\n```\n\nAs per the given HTML snippet, remember to place the data attributes for values on the same element as the `data-controller` attribute.\n\n<meta data-controller=\"callout\" data-callout-text-value=\"static values = { url: String }\">\n<meta data-controller=\"callout\" data-callout-text-value=\"this.urlValue\">\n\n```js\n// controllers/loader_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static values = {\n    url: String\n  }\n\n  connect() {\n    fetch(this.urlValue).then(/* … */)\n  }\n}\n```\n\n## Definitions\n\nDefine values in a controller using the `static values` object. Put each value's _name_ on the left and its _type_ on the right.\n\n```js\nexport default class extends Controller {\n  static values = {\n    url: String,\n    interval: Number,\n    params: Object\n  }\n\n  // …\n}\n```\n\n## Types\n\nA value's type is one of `Array`, `Boolean`, `Number`, `Object`, or `String`. The type determines how the value is transcoded between JavaScript and HTML.\n\n| Type    | Encoded as…              | Decoded as…                             |\n| ------- | ------------------------ | --------------------------------------- |\n| Array   | `JSON.stringify(array)`  | `JSON.parse(value)`                     |\n| Boolean | `boolean.toString()`     | `!(value == \"0\" \\|\\| value == \"false\")` |\n| Number  | `number.toString()`      | `Number(value.replace(/_/g, \"\"))`       |\n| Object  | `JSON.stringify(object)` | `JSON.parse(value)`                     |\n| String  | Itself                   | Itself                                  |\n\n## Properties and Attributes\n\nStimulus automatically generates getter, setter, and existential properties for each value defined in a controller. These properties are linked to data attributes on the controller's element:\n\nKind | Property name | Effect\n---- | ------------- | ------\nGetter | `this.[name]Value` | Reads `data-[identifier]-[name]-value`\nSetter | `this.[name]Value=` | Writes `data-[identifier]-[name]-value`\nExistential | `this.has[Name]Value` | Tests for `data-[identifier]-[name]-value`\n\n### Getters\n\nThe getter for a value decodes the associated data attribute into an instance of the value's type.\n\nIf the data attribute is missing from the controller's element, the getter returns a _default value_, depending on the value's type:\n\nType | Default value\n---- | -------------\nArray | `[]`\nBoolean | `false`\nNumber | `0`\nObject | `{}`\nString | `\"\"`\n\n### Setters\n\nThe setter for a value sets the associated data attribute on the controller's element.\n\nTo remove the data attribute from the controller's element, assign `undefined` to the value.\n\n### Existential Properties\n\nThe existential property for a value evaluates to `true` when the associated data attribute is present on the controller's element and `false` when it is absent.\n\n## Change Callbacks\n\nValue _change callbacks_ let you respond whenever a value's data attribute changes.\n\nDefine a method `[name]ValueChanged` in the controller, where `[name]` is the name of the value you want to observe for changes. The method receives its decoded value as the first argument and the decoded previous value as the second argument.\n\nStimulus invokes each change callback after the controller is initialized and again any time its associated data attribute changes. This includes changes as a result of assignment to the value's setter.\n\n```js\nexport default class extends Controller {\n  static values = { url: String }\n\n  urlValueChanged() {\n    fetch(this.urlValue).then(/* … */)\n  }\n}\n```\n\n### Previous Values\n\nYou can access the previous value of a `[name]ValueChanged` callback by defining the callback method with two arguments in your controller.\n\n```js\nexport default class extends Controller {\n  static values = { url: String }\n\n  urlValueChanged(value, previousValue) {\n    /* … */\n  }\n}\n```\n\nThe two arguments can be named as you like. You could also use `urlValueChanged(current, old)`.\n\n## Default Values\n\nValues that have not been specified on the controller element can be set by defaults specified in the controller definition:\n\n```js\nexport default class extends Controller {\n  static values = {\n    url: { type: String, default: '/bill' },\n    interval: { type: Number, default: 5 },\n    clicked: Boolean\n  }\n}\n```\n\nWhen a default is used, the expanded form of `{ type, default }` is used. This form can be mixed with the regular form that does not use a default.\n\n## Naming Conventions\n\nWrite value names as camelCase in JavaScript and kebab-case in HTML. For example, a value named `contentType` in the `loader` controller will have the associated data attribute `data-loader-content-type-value`.\n"
  },
  {
    "path": "examples/.babelrc",
    "content": "{\n  \"presets\": [\n    [\"@babel/preset-env\"]\n  ],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-class-properties\"],\n    [\"@babel/plugin-transform-runtime\"]\n  ]\n}\n"
  },
  {
    "path": "examples/controllers/clipboard_controller.js",
    "content": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"source\" ]\n  static classes = [ \"supported\" ]\n\n  initialize() {\n    if (document.queryCommandSupported(\"copy\")) {\n      this.element.classList.add(this.supportedClass)\n    }\n  }\n\n  copy() {\n    navigator.clipboard.writeText(this.sourceTarget.value)\n  }\n}\n"
  },
  {
    "path": "examples/controllers/content_loader_controller.js",
    "content": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [\"item\"]\n  static values = { url: String, refreshInterval: Number }\n\n  connect() {\n    this.load()\n\n    if (this.hasRefreshIntervalValue) {\n      this.startRefreshing()\n    }\n  }\n\n  itemTargetConnected(target) {\n    console.log(\"itemTargetConnected:\", target)\n  }\n\n  itemTargetDisconnected(target) {\n    console.log(\"itemTargetDisconnected:\", target)\n  }\n\n  disconnect() {\n    this.stopRefreshing()\n  }\n\n  load() {\n    fetch(this.urlValue)\n      .then(response => response.text())\n      .then(html => {\n        this.element.innerHTML = html\n      })\n  }\n\n  startRefreshing() {\n    this.refreshTimer = setInterval(() => {\n      this.load()\n    }, this.refreshIntervalValue)\n  }\n\n  stopRefreshing() {\n    if (this.refreshTimer) {\n      clearInterval(this.refreshTimer)\n    }\n  }\n}\n"
  },
  {
    "path": "examples/controllers/hello_controller.js",
    "content": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [\"name\"]\n\n  greet() {\n    alert(`Hello, ${this.name}!`)\n  }\n\n  get name() {\n    return this.nameTarget.value\n  }\n}\n"
  },
  {
    "path": "examples/controllers/slideshow_controller.js",
    "content": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"slide\" ]\n  static classes = [ \"currentSlide\" ]\n  static values = { index: Number }\n\n  next() {\n    if (this.indexValue < this.lastIndex) {\n      this.indexValue++\n    }\n  }\n\n  previous() {\n    if (this.indexValue > 0) {\n      this.indexValue--\n    }\n  }\n\n  indexValueChanged() {\n    this.render()\n  }\n\n  render() {\n    this.slideTargets.forEach((element, index) => {\n      element.classList.toggle(this.currentSlideClass, index == this.indexValue)\n    })\n  }\n\n  get lastIndex() {\n    return this.slideTargets.length - 1\n  }\n}\n"
  },
  {
    "path": "examples/controllers/tabs_controller.js",
    "content": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static targets = [ \"tab\", \"tabpanel\" ]\n  static classes = [ \"current\" ]\n  static values = { index: { default: 0, type: Number } }\n\n  next() {\n    if (this.indexValue < this.lastIndex) {\n      this.indexValue++\n      return\n    }\n    this.indexValue = 0\n  }\n\n  previous() {\n    if (this.indexValue > 0) {\n      this.indexValue--\n      return\n    }\n    this.indexValue = this.lastIndex\n  }\n\n  open(evt) {\n    this.indexValue = this.tabTargets.indexOf(evt.currentTarget)\n  }\n\n  get lastIndex() {\n    return this.tabTargets.length - 1\n  }\n\n  indexValueChanged(current, old) {\n    let panels = this.tabpanelTargets\n    let tabs = this.tabTargets\n\n    if (old != null) {\n      panels[old].classList.remove(...this.currentClasses)\n      tabs[old].tabIndex = -1\n    }\n    panels[current].classList.add(...this.currentClasses)\n    tabs[current].tabIndex = 0\n    tabs[current].focus()\n  }\n}\n"
  },
  {
    "path": "examples/index.js",
    "content": "import { Application } from \"@hotwired/stimulus\"\nimport \"@hotwired/turbo\"\n\nconst application = Application.start()\n\nimport ClipboardController from \"./controllers/clipboard_controller\"\napplication.register(\"clipboard\", ClipboardController)\n\nimport ContentLoaderController from \"./controllers/content_loader_controller\"\napplication.register(\"content-loader\", ContentLoaderController)\n\nimport HelloController from \"./controllers/hello_controller\"\napplication.register(\"hello\", HelloController)\n\nimport SlideshowController from \"./controllers/slideshow_controller\"\napplication.register(\"slideshow\", SlideshowController)\n\nimport TabsController from \"./controllers/tabs_controller\"\napplication.register(\"tabs\", TabsController)\n"
  },
  {
    "path": "examples/package.json",
    "content": "{\n  \"name\": \"@hotwired/stimulus-examples\",\n  \"version\": \"3.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@babel/core\": \"^7.5.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.5.5\",\n    \"@babel/plugin-transform-runtime\": \"^7.14.5\",\n    \"@babel/preset-env\": \"^7.5.5\",\n    \"@hotwired/turbo\": \"^7.2.4\",\n    \"babel-loader\": \"^8.0.6\",\n    \"ejs\": \"^3.1.10\",\n    \"express\": \"^4.20.0\",\n    \"webpack\": \"^4.39.1\",\n    \"webpack-dev-middleware\": \"^5.3.4\"\n  },\n  \"scripts\": {\n    \"start\": \"node server.js\"\n  }\n}\n"
  },
  {
    "path": "examples/public/examples.css",
    "content": "body {\n  background: rgb(251, 247, 240);\n}\n\nmain {\n  display: flex;\n  flex-flow: row;\n  align-items: flex-start;\n  justify-content: flex-start;\n}\n\n.logo {\n  width: 6ex;\n  height: 6ex;\n  margin: 0 0 1ex;\n}\n\n.sidebar {\n  width: 10em;\n  margin-top: 3ex;\n  margin-left: 2em;\n}\n\n.nav {\n  white-space: nowrap;\n}\n\n.nav li {\n  list-style: none;\n}\n\n.nav a {\n  display: block;\n  position: relative;\n  text-decoration: none;\n}\n\n.nav a.active {\n  font-weight: bold;\n  color: #000;\n}\n\n.nav a.active::before {\n  content: \"►\";\n  display: inline-block;\n  position: absolute;\n  left: -1em;\n  font-size: 1em;\n}\n\n.container {\n  flex-grow: 0;\n  background-color: #fff;\n  border: 0.25ex solid #333;\n  border-radius: 1ex;\n  box-shadow: 1ex 1ex 0 0 rgba(0, 0, 0, 0.25);\n  margin: 7ex 3em 0 0;\n  padding: 2.5ex 2em;\n  display: flex;\n  flex-direction: column;\n}\n\n.clipboard-button {\n  display: none;\n}\n\n.clipboard--supported .clipboard-button {\n  display: initial;\n}\n\n.clipboard-source {\n  background-color: transparent;\n  border: none;\n  box-shadow: none;\n  color: inherit;\n  font-size: inherit;\n}\n\n.clipboard-paste {\n  min-width: 20em;\n  min-height: 8ex;\n}\n\n.slide {\n  display: none;\n  font-size: 6rem;\n}\n\n.slide--current {\n  display: block;\n}\n\n.container--content-loader {\n  min-width: 16em;\n}\n\n.tabpanel {\n  border: 1px solid #dedede;\n  display: none;\n  margin-top: .4rem;\n  padding: 0.8rem;\n  font-size: 6rem;\n}\n.tabpanel--current {\n  display: block;\n}\n"
  },
  {
    "path": "examples/public/main.css",
    "content": "* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\n*:not(:active) {\n  color: #333;\n}\n\na:active {\n  color: #333;\n}\n\nbody {\n  background: #fff;\n  font-family: -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;\n  font-size: 16px;\n  padding: 2ex 0;\n  line-height: 1.5;\n}\n\np {\n  margin-bottom: 2ex;\n}\n\ntextarea {\n  resize: none;\n}\n\nbutton,\ninput,\ntextarea {\n  background: #fff;\n  border: 0.25ex solid #333;\n  border-radius: 0.5em;\n  margin: 0.5ex 0.5em;\n  padding: 0.5ex 0.5em;\n  font-family: inherit;\n  font-size: 16px;\n  box-shadow: 0 0 0 0.5ex rgba(0, 0, 0, 0.25);\n  vertical-align: middle;\n}\n\nbutton {\n  background: #ccc;\n  border-left-color: #fff;\n  border-top-color: #fff;\n}\n\nbutton:active {\n  border-left-color: #333;\n  border-top-color: #333;\n  border-bottom-color: #ccc;\n  border-right-color: #ccc;\n  color: #333;\n  padding: calc(0.5ex + 1px) calc(0.5em - 1px) calc(0.5ex - 1px) calc(0.5em + 1px);\n  margin: calc(0.5ex - 1px) calc(0.5em + 1px) calc(0.5ex + 1px) calc(0.5em - 1px);\n  transform: translate(1px, 1px);\n}\n\nbutton:active,\nbutton:focus {\n  outline: none;\n}\n\nbutton:focus-visible,\ninput:focus-visible,\ntextarea:focus-visible {\n  outline: none;\n  box-shadow: 0 0 0 0.5ex rgba(0, 51, 255, 0.5);\n}\n\ninput,\ntextarea {\n  border-bottom-color: #ccc;\n  border-right-color: #ccc;\n}\n"
  },
  {
    "path": "examples/server.js",
    "content": "const fs = require(\"fs\")\nconst path = require(\"path\")\nconst express = require(\"express\")\nconst webpack = require(\"webpack\")\nconst webpackMiddleware = require(\"webpack-dev-middleware\")\nconst webpackConfig = require(\"./webpack.config\")\n\nconst app = express()\nconst port = 9000\nconst publicPath = path.join(__dirname, \"public\")\nconst viewPath = path.join(__dirname, \"views\")\nconst viewEngine = \"ejs\"\n\napp.set(\"views\", viewPath)\napp.set(\"view engine\", viewEngine)\n\napp.use(express.static(publicPath))\napp.use(webpackMiddleware(webpack(webpackConfig)))\n\nconst pages = [\n  { path: \"/hello\", title: \"Hello\" },\n  { path: \"/clipboard\", title: \"Clipboard\" },\n  { path: \"/slideshow\", title: \"Slideshow\" },\n  { path: \"/content-loader\", title: \"Content Loader\" },\n  { path: \"/tabs\", title: \"Tabs\" },\n]\n\napp.get(\"/\", (req, res) => {\n  res.redirect(pages[0].path)\n})\n\napp.get(\"/uptime\", (req, res, next) => {\n  res.send(`<span data-content-loader-target=\"item\">${process.uptime().toString()}</span>`)\n})\n\napp.get(\"/:page\", (req, res, next) => {\n  const currentPage = pages.find(page => page.path == req.path)\n  res.render(req.params.page, { pages, currentPage })\n})\n\napp.listen(port, () => {\n  console.log(`Listening on http://localhost:${port}/`)\n})\n"
  },
  {
    "path": "examples/views/clipboard.ejs",
    "content": "<%- include(\"layout/head\") %>\n\n<p data-controller=\"clipboard\" data-clipboard-supported-class=\"clipboard--supported\">\n  <input data-clipboard-target=\"source\" class=\"clipboard-source\" type=\"text\" value=\"https://stimulus.hotwired.dev\" readonly>\n  <button data-action=\"clipboard#copy\" class=\"clipboard-button\">Copy</button>\n</p>\n\n<p data-controller=\"clipboard\" data-clipboard-supported-class=\"clipboard--supported\">\n  <input data-clipboard-target=\"source\" class=\"clipboard-source\" type=\"text\" value=\"https://basecamp.com\" readonly>\n  <button data-action=\"clipboard#copy\" class=\"clipboard-button\">Copy</button>\n</p>\n\n<br>\n\n<p>\n  <textarea class=\"clipboard-paste\" placeholder=\"Paste here…\"></textarea>\n</p>\n\n<%- include(\"layout/tail\") %>\n"
  },
  {
    "path": "examples/views/content-loader.ejs",
    "content": "<%- include(\"layout/head\") %>\n\n<p data-controller=\"content-loader\"\n   data-content-loader-url-value=\"/uptime\"></p>\n\n<p data-controller=\"content-loader\"\n   data-content-loader-url-value=\"/uptime\"\n   data-content-loader-refresh-interval-value=\"3500\"></p>\n\n<p data-controller=\"content-loader\"\n   data-content-loader-url-value=\"/uptime\"\n   data-content-loader-refresh-interval-value=\"1000\"></p>\n\n<%- include(\"layout/tail\") %>\n"
  },
  {
    "path": "examples/views/hello.ejs",
    "content": "<%- include(\"layout/head\") %>\n\n<form method=\"dialog\" data-controller=\"hello\">\n  <p>\n    <input data-hello-target=\"name\" type=\"text\" placeholder=\"Type your name…\" autofocus>\n    <button data-action=\"click->hello#greet\">Greet</button>\n  </p>\n</form>\n\n<%- include(\"layout/tail\") %>\n"
  },
  {
    "path": "examples/views/layout/head.ejs",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Stimulus: <%= currentPage.title %> Example</title>\n  <link rel=\"stylesheet\" href=\"/main.css\">\n  <link rel=\"stylesheet\" href=\"/examples.css\">\n  <script src=\"/main.js\" defer></script>\n</head>\n\n<body>\n  <main>\n    <div class=\"sidebar\">\n      <img id=\"logo\" src=\"/logo.svg\" class=\"logo\" alt=\"Stimulus logo\" data-turbolinks-permanent>\n\n      <ul class=\"nav\">\n        <% pages.forEach(page => { %>\n          <li>\n            <a href=\"<%= page.path %>\" class=\"<%= page.path == currentPage.path ? \" active \" : \" \" %>\">\n              <%= page.title %>\n            </a>\n          </li>\n          <% }) %>\n      </ul>\n    </div>\n\n    <div class=\"container container--<%= currentPage.path.slice(1) %>\">\n"
  },
  {
    "path": "examples/views/layout/tail.ejs",
    "content": "    </div>\n  </main>\n</body>\n</html>\n"
  },
  {
    "path": "examples/views/slideshow.ejs",
    "content": "<%- include(\"layout/head\") %>\n\n<div data-controller=\"slideshow\" data-slideshow-current-slide-class=\"slide--current\">\n  <button data-action=\"slideshow#previous\">←</button>\n  <button data-action=\"slideshow#next\">→</button>\n\n  <div data-slideshow-target=\"slide\" class=\"slide\">🐵</div>\n  <div data-slideshow-target=\"slide\" class=\"slide\">🙈</div>\n  <div data-slideshow-target=\"slide\" class=\"slide\">🙉</div>\n  <div data-slideshow-target=\"slide\" class=\"slide\">🙊</div>\n</div>\n\n<%- include(\"layout/tail\") %>\n"
  },
  {
    "path": "examples/views/tabs.ejs",
    "content": "<%- include(\"layout/head\") %>\n\n<div data-controller=\"tabs\" data-tabs-current-class=\"tabpanel--current\" aria-label=\"example\">\n  <p>This tabbed interface is operated by focusing on a button and pressing the left and right keys.</p>\n  <div role=\"tablist\">\n    <button\n      id=\"tab1\"\n      role=\"tab\"\n      tabindex=\"0\"\n      data-action=\"keydown.left->tabs#previous keydown.right->tabs#next click->tabs#open\"\n      data-tabs-target=\"tab\"\n      aria-controls=\"panel1\"\n    >tab1</button>\n    <button\n      id=\"tab2\"\n      role=\"tab\"\n      tabindex=\"0\"\n      data-action=\"keydown.left->tabs#previous keydown.right->tabs#next click->tabs#open\"\n      data-tabs-target=\"tab\"\n      aria-controls=\"panel2\"\n    >tab2</button>\n  </div>\n\n  <div\n    id=\"panel1\"\n    role=\"tabpanel\"\n    tabindex=\"0\"\n    data-tabs-target=\"tabpanel\"\n    class=\"tabpanel tabpanel--current\"\n    aria-labelledby=\"tab1\"\n  >🐵</div>\n  <div\n    id=\"panel2\"\n    role=\"tabpanel\"\n    tabindex=\"0\"\n    data-tabs-target=\"tabpanel\"\n    class=\"tabpanel\"\n    aria-labelledby=\"tab2\"\n  >🙈</div>\n</div>\n\n<%- include(\"layout/tail\") %>\n"
  },
  {
    "path": "examples/webpack.config.js",
    "content": "const path = require(\"path\")\n\nmodule.exports = {\n  entry: {\n    main: \"./index.js\"\n  },\n\n  output: {\n    filename: \"[name].js\"\n  },\n\n  mode: \"development\",\n  devtool: \"inline-source-map\",\n\n  module: {\n    rules: [\n      {\n        test: /\\.ts$/,\n        use: [\n          { loader: \"ts-loader\" }\n        ]\n      },\n      {\n        test: /\\.js$/,\n        exclude: [\n          /node_modules/\n        ],\n        use: [\n          { loader: \"babel-loader\" }\n        ]\n      }\n    ]\n  },\n\n  resolve: {\n    extensions: [\".ts\", \".js\"],\n    modules: [\"src\", \"node_modules\"],\n    alias: {\n      \"@hotwired/stimulus\": path.resolve(__dirname, \"../dist/stimulus.js\"),\n    }\n  }\n}\n"
  },
  {
    "path": "karma.conf.cjs",
    "content": "const config = {\n  basePath: \".\",\n\n  browsers: [\"ChromeHeadless\", \"FirefoxHeadless\"],\n\n  frameworks: [\"qunit\"],\n\n  reporters: [\"progress\"],\n\n  singleRun: true,\n\n  autoWatch: false,\n\n  files: [\n    \"dist/tests/index.js\",\n    { pattern: \"src/tests/fixtures/**/*\", watched: true, served: true, included: false },\n    { pattern: \"dist/tests/fixtures/**/*\", watched: true, served: true, included: false },\n  ],\n\n  preprocessors: {\n    \"dist/tests/**/*.js\": [\"webpack\"],\n  },\n\n  webpack: {\n    mode: \"development\",\n    resolve: {\n      extensions: [\".js\"],\n    },\n  },\n\n  client: {\n    clearContext: false,\n    qunit: {\n      showUI: true,\n    },\n  },\n\n  hostname: \"0.0.0.0\",\n\n  captureTimeout: 180000,\n  browserDisconnectTimeout: 180000,\n  browserDisconnectTolerance: 3,\n  browserNoActivityTimeout: 300000,\n}\n\nmodule.exports = function (karmaConfig) {\n  karmaConfig.set(config)\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@hotwired/stimulus\",\n  \"version\": \"3.2.2\",\n  \"license\": \"MIT\",\n  \"description\": \"A modest JavaScript framework for the HTML you already have.\",\n  \"author\": \"Basecamp, LLC\",\n  \"contributors\": [\n    \"David Heinemeier Hansson <david@basecamp.com>\",\n    \"Javan Makhmali <javan@javan.us>\",\n    \"Sam Stephenson <sstephenson@gmail.com>\"\n  ],\n  \"homepage\": \"https://stimulus.hotwired.dev\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/hotwired/stimulus.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/hotwired/stimulus/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"module\": \"dist/stimulus.js\",\n  \"main\": \"dist/stimulus.umd.js\",\n  \"types\": \"dist/types/index.d.ts\",\n  \"files\": [\n    \"dist/stimulus.js\",\n    \"dist/stimulus.umd.js\",\n    \"dist/types/**/*\"\n  ],\n  \"scripts\": {\n    \"clean\": \"rm -fr dist\",\n    \"types\": \"tsc --noEmit false --declaration true --emitDeclarationOnly true --outDir dist/types\",\n    \"prebuild\": \"yarn build:test\",\n    \"build\": \"yarn types && rollup -c\",\n    \"build:test\": \"tsc -b tsconfig.test.json\",\n    \"watch\": \"rollup -wc\",\n    \"prerelease\": \"yarn clean && yarn build && yarn build:test && git --no-pager diff && echo && npm pack --dry-run\",\n    \"release\": \"npm publish\",\n    \"start\": \"concurrently \\\"npm:watch\\\" \\\"npm:start:examples\\\"\",\n    \"start:examples\": \"cd examples && yarn install && node server.js\",\n    \"test\": \"yarn build:test && karma start karma.conf.cjs\",\n    \"test:watch\": \"yarn test --auto-watch --no-single-run\",\n    \"lint\": \"eslint . --ext .ts\",\n    \"format\": \"yarn lint --fix\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-node-resolve\": \"^16.0.1\",\n    \"@rollup/plugin-typescript\": \"^11.1.1\",\n    \"@types/qunit\": \"^2.19.10\",\n    \"@types/webpack-env\": \"^1.14.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.59.11\",\n    \"@typescript-eslint/parser\": \"^5.59.11\",\n    \"concurrently\": \"^9.1.2\",\n    \"eslint\": \"^8.43.0\",\n    \"eslint-config-prettier\": \"^8.8.0\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"karma\": \"^6.4.4\",\n    \"karma-chrome-launcher\": \"^3.2.0\",\n    \"karma-firefox-launcher\": \"^2.1.3\",\n    \"karma-qunit\": \"^4.2.1\",\n    \"karma-webpack\": \"^4.0.2\",\n    \"prettier\": \"^2.8.8\",\n    \"qunit\": \"^2.20.0\",\n    \"rollup\": \"^2.53\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"ts-loader\": \"^9.4.3\",\n    \"tslib\": \"^2.5.3\",\n    \"typescript\": \"^5.1.3\",\n    \"webpack\": \"^4.47.0\"\n  },\n  \"resolutions\": {\n    \"webdriverio\": \"^7.19.5\"\n  }\n}\n"
  },
  {
    "path": "packages/stimulus/.gitignore",
    "content": "README.md\n"
  },
  {
    "path": "packages/stimulus/.npmignore",
    "content": "rollup.config.js\n*.log\n"
  },
  {
    "path": "packages/stimulus/index.d.ts",
    "content": "export * from \"@hotwired/stimulus\"\nexport as namespace Stimulus\n"
  },
  {
    "path": "packages/stimulus/index.js",
    "content": "export * from \"@hotwired/stimulus\"\n"
  },
  {
    "path": "packages/stimulus/package.json",
    "content": "{\n  \"name\": \"stimulus\",\n  \"version\": \"3.2.2\",\n  \"description\": \"Stimulus JavaScript framework\",\n  \"homepage\": \"https://stimulus.hotwired.dev\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/hotwired/stimulus.git\"\n  },\n  \"author\": \"Basecamp, LLC\",\n  \"contributors\": [\n    \"David Heinemeier Hansson <david@basecamp.com>\",\n    \"Javan Makhmali <javan@javan.us>\",\n    \"Sam Stephenson <sstephenson@gmail.com>\"\n  ],\n  \"module\": \"./dist/stimulus.js\",\n  \"main\": \"./dist/stimulus.umd.js\",\n  \"types\": \"./index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"main\": \"./dist/stimulus.umd.js\",\n      \"browser\": \"./dist/stimulus.js\",\n      \"import\": \"./dist/stimulus.js\",\n      \"module\": \"./dist/stimulus.js\",\n      \"umd\": \"./dist/stimulus.umd.js\",\n      \"types\": \"./index.d.ts\"\n    },\n    \"./webpack-helpers\": {\n      \"main\": \"./dist/webpack-helpers.umd.js\",\n      \"browser\": \"./dist/webpack-helpers.js\",\n      \"import\": \"./dist/webpack-helpers.js\",\n      \"module\": \"./dist/webpack-helpers.js\",\n      \"umd\": \"./dist/webpack-helpers.umd.js\",\n      \"types\": \"./webpack-helpers.d.ts\"\n    }\n  },\n  \"files\": [\n    \"index.d.ts\",\n    \"dist/stimulus.js\",\n    \"dist/stimulus.umd.js\",\n    \"webpack-helpers.js\",\n    \"webpack-helpers.d.ts\",\n    \"dist/webpack-helpers.js\",\n    \"dist/webpack-helpers.umd.js\",\n    \"README.md\"\n  ],\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@hotwired/stimulus\": \"^3.2.2\",\n    \"@hotwired/stimulus-webpack-helpers\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-node-resolve\": \"^13.0.0\",\n    \"@rollup/plugin-typescript\": \"^8.2.1\",\n    \"rollup\": \"^2.53\"\n  },\n  \"scripts\": {\n    \"clean\": \"rm -rf dist\",\n    \"build\": \"rollup --config rollup.config.js\",\n    \"prerelease\": \"yarn build && git --no-pager diff && echo && npm pack --dry-run\",\n    \"release\": \"npm publish\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "packages/stimulus/rollup.config.js",
    "content": "import resolve from \"@rollup/plugin-node-resolve\"\n\nexport default [\n  {\n    input: \"index.js\",\n    output: [\n      {\n        name: \"Stimulus\",\n        file: \"dist/stimulus.umd.js\",\n        format: \"umd\"\n      },\n      {\n        file: \"dist/stimulus.js\",\n        format: \"es\"\n      },\n    ],\n    context: \"window\",\n    plugins: [\n      resolve()\n    ]\n  },\n  {\n    input: \"webpack-helpers.js\",\n    output: [\n      {\n        name: \"StimulusWebpackHelpers\",\n        file: \"dist/webpack-helpers.umd.js\",\n        format: \"umd\"\n      },\n      {\n        file: \"dist/webpack-helpers.js\",\n        format: \"es\"\n      },\n    ],\n    context: \"window\",\n    plugins: [\n      resolve()\n    ]\n  }\n]\n"
  },
  {
    "path": "packages/stimulus/webpack-helpers.d.ts",
    "content": "export * from \"@hotwired/stimulus-webpack-helpers\"\nexport as namespace StimulusWebpackHelpers\n"
  },
  {
    "path": "packages/stimulus/webpack-helpers.js",
    "content": "export * from \"@hotwired/stimulus-webpack-helpers\"\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import resolve from \"@rollup/plugin-node-resolve\"\nimport typescript from \"@rollup/plugin-typescript\"\nimport { terser } from \"rollup-plugin-terser\"\nimport { version } from \"./package.json\"\n\nconst year = new Date().getFullYear()\nconst banner = `/*\\nStimulus ${version}\\nCopyright © ${year} Basecamp, LLC\\n */`\n\nexport default [\n  {\n    input: \"src/index.js\",\n    output: [\n      {\n        name: \"Stimulus\",\n        file: \"dist/stimulus.umd.js\",\n        format: \"umd\",\n        banner\n      },\n      {\n        file: \"dist/stimulus.js\",\n        format: \"es\",\n        banner\n      },\n    ],\n    context: \"window\",\n    plugins: [\n      resolve(),\n      typescript()\n    ]\n  },\n  {\n    input: \"src/index.js\",\n    output: {\n      file: \"dist/stimulus.min.js\",\n      format: \"es\",\n      banner,\n      sourcemap: true\n    },\n    context: \"window\",\n    plugins: [\n      resolve(),\n      typescript(),\n      terser({\n        mangle: true,\n        compress: true\n      })\n    ]\n  }\n]"
  },
  {
    "path": "src/core/action.ts",
    "content": "import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from \"./action_descriptor\"\nimport { Token } from \"../mutation-observers\"\nimport { Schema } from \"./schema\"\nimport { camelize } from \"./string_helpers\"\nimport { hasProperty } from \"./utils\"\n\nconst allModifiers = [\"meta\", \"ctrl\", \"alt\", \"shift\"]\n\nexport class Action {\n  readonly element: Element\n  readonly index: number\n  readonly eventTarget: EventTarget\n  readonly eventName: string\n  readonly eventOptions: AddEventListenerOptions\n  readonly identifier: string\n  readonly methodName: string\n  readonly keyFilter: string\n  readonly schema: Schema\n\n  static forToken(token: Token, schema: Schema) {\n    return new this(token.element, token.index, parseActionDescriptorString(token.content), schema)\n  }\n\n  constructor(element: Element, index: number, descriptor: Partial<ActionDescriptor>, schema: Schema) {\n    this.element = element\n    this.index = index\n    this.eventTarget = descriptor.eventTarget || element\n    this.eventName = descriptor.eventName || getDefaultEventNameForElement(element) || error(\"missing event name\")\n    this.eventOptions = descriptor.eventOptions || {}\n    this.identifier = descriptor.identifier || error(\"missing identifier\")\n    this.methodName = descriptor.methodName || error(\"missing method name\")\n    this.keyFilter = descriptor.keyFilter || \"\"\n    this.schema = schema\n  }\n\n  toString() {\n    const eventFilter = this.keyFilter ? `.${this.keyFilter}` : \"\"\n    const eventTarget = this.eventTargetName ? `@${this.eventTargetName}` : \"\"\n    return `${this.eventName}${eventFilter}${eventTarget}->${this.identifier}#${this.methodName}`\n  }\n\n  shouldIgnoreKeyboardEvent(event: KeyboardEvent): boolean {\n    if (!this.keyFilter) {\n      return false\n    }\n\n    const filters = this.keyFilter.split(\"+\")\n    if (this.keyFilterDissatisfied(event, filters)) {\n      return true\n    }\n\n    const standardFilter = filters.filter((key) => !allModifiers.includes(key))[0]\n    if (!standardFilter) {\n      // missing non modifier key\n      return false\n    }\n\n    if (!hasProperty(this.keyMappings, standardFilter)) {\n      error(`contains unknown key filter: ${this.keyFilter}`)\n    }\n\n    return this.keyMappings[standardFilter].toLowerCase() !== event.key.toLowerCase()\n  }\n\n  shouldIgnoreMouseEvent(event: MouseEvent): boolean {\n    if (!this.keyFilter) {\n      return false\n    }\n\n    const filters = [this.keyFilter]\n    if (this.keyFilterDissatisfied(event, filters)) {\n      return true\n    }\n\n    return false\n  }\n\n  get params() {\n    const params: { [key: string]: any } = {}\n    const pattern = new RegExp(`^data-${this.identifier}-(.+)-param$`, \"i\")\n\n    for (const { name, value } of Array.from(this.element.attributes)) {\n      const match = name.match(pattern)\n      const key = match && match[1]\n      if (key) {\n        params[camelize(key)] = typecast(value)\n      }\n    }\n    return params\n  }\n\n  private get eventTargetName() {\n    return stringifyEventTarget(this.eventTarget)\n  }\n\n  private get keyMappings() {\n    return this.schema.keyMappings\n  }\n\n  private keyFilterDissatisfied(event: KeyboardEvent | MouseEvent, filters: Array<string>): boolean {\n    const [meta, ctrl, alt, shift] = allModifiers.map((modifier) => filters.includes(modifier))\n\n    return event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift\n  }\n}\n\nconst defaultEventNames: { [tagName: string]: (element: Element) => string } = {\n  a: () => \"click\",\n  button: () => \"click\",\n  form: () => \"submit\",\n  details: () => \"toggle\",\n  input: (e) => (e.getAttribute(\"type\") == \"submit\" ? \"click\" : \"input\"),\n  select: () => \"change\",\n  textarea: () => \"input\",\n}\n\nexport function getDefaultEventNameForElement(element: Element): string | undefined {\n  const tagName = element.tagName.toLowerCase()\n  if (tagName in defaultEventNames) {\n    return defaultEventNames[tagName](element)\n  }\n}\n\nfunction error(message: string): never {\n  throw new Error(message)\n}\n\nfunction typecast(value: any): any {\n  try {\n    return JSON.parse(value)\n  } catch (o_O) {\n    return value\n  }\n}\n"
  },
  {
    "path": "src/core/action_descriptor.ts",
    "content": "import type { Controller } from \"./controller\"\n\nexport type ActionDescriptorFilters = Record<string, ActionDescriptorFilter>\nexport type ActionDescriptorFilter = (options: ActionDescriptorFilterOptions) => boolean\ntype ActionDescriptorFilterOptions = {\n  name: string\n  value: boolean\n  event: Event\n  element: Element\n  controller: Controller<Element>\n}\n\nexport const defaultActionDescriptorFilters: ActionDescriptorFilters = {\n  stop({ event, value }) {\n    if (value) event.stopPropagation()\n\n    return true\n  },\n\n  prevent({ event, value }) {\n    if (value) event.preventDefault()\n\n    return true\n  },\n\n  self({ event, value, element }) {\n    if (value) {\n      return element === event.target\n    } else {\n      return true\n    }\n  },\n}\n\nexport interface ActionDescriptor {\n  eventTarget: EventTarget\n  eventOptions: AddEventListenerOptions\n  eventName: string\n  identifier: string\n  methodName: string\n  keyFilter: string\n}\n\n// capture nos.:                  1      1    2   2     3   3      4               4      5   5    6      6     7  7\nconst descriptorPattern = /^(?:(?:([^.]+?)\\+)?(.+?)(?:\\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/\n\nexport function parseActionDescriptorString(descriptorString: string): Partial<ActionDescriptor> {\n  const source = descriptorString.trim()\n  const matches = source.match(descriptorPattern) || []\n  let eventName = matches[2]\n  let keyFilter = matches[3]\n\n  if (keyFilter && ![\"keydown\", \"keyup\", \"keypress\"].includes(eventName)) {\n    eventName += `.${keyFilter}`\n    keyFilter = \"\"\n  }\n\n  return {\n    eventTarget: parseEventTarget(matches[4]),\n    eventName,\n    eventOptions: matches[7] ? parseEventOptions(matches[7]) : {},\n    identifier: matches[5],\n    methodName: matches[6],\n    keyFilter: matches[1] || keyFilter,\n  }\n}\n\nfunction parseEventTarget(eventTargetName: string): EventTarget | undefined {\n  if (eventTargetName == \"window\") {\n    return window\n  } else if (eventTargetName == \"document\") {\n    return document\n  }\n}\n\nfunction parseEventOptions(eventOptions: string): AddEventListenerOptions {\n  return eventOptions\n    .split(\":\")\n    .reduce((options, token) => Object.assign(options, { [token.replace(/^!/, \"\")]: !/^!/.test(token) }), {})\n}\n\nexport function stringifyEventTarget(eventTarget: EventTarget) {\n  if (eventTarget == window) {\n    return \"window\"\n  } else if (eventTarget == document) {\n    return \"document\"\n  }\n}\n"
  },
  {
    "path": "src/core/action_event.ts",
    "content": "export interface ActionEvent extends Event {\n  params: { [key: string]: any }\n}\n"
  },
  {
    "path": "src/core/application.ts",
    "content": "import { Controller, ControllerConstructor } from \"./controller\"\nimport { Definition } from \"./definition\"\nimport { Dispatcher } from \"./dispatcher\"\nimport { ErrorHandler } from \"./error_handler\"\nimport { Logger } from \"./logger\"\nimport { Router } from \"./router\"\nimport { Schema, defaultSchema } from \"./schema\"\nimport { ActionDescriptorFilter, ActionDescriptorFilters, defaultActionDescriptorFilters } from \"./action_descriptor\"\n\nexport class Application implements ErrorHandler {\n  readonly element: Element\n  readonly schema: Schema\n  readonly dispatcher: Dispatcher\n  readonly router: Router\n  readonly actionDescriptorFilters: ActionDescriptorFilters\n  logger: Logger = console\n  debug = false\n\n  static start(element?: Element, schema?: Schema): Application {\n    const application = new this(element, schema)\n    application.start()\n    return application\n  }\n\n  constructor(element: Element = document.documentElement, schema: Schema = defaultSchema) {\n    this.element = element\n    this.schema = schema\n    this.dispatcher = new Dispatcher(this)\n    this.router = new Router(this)\n    this.actionDescriptorFilters = { ...defaultActionDescriptorFilters }\n  }\n\n  async start() {\n    await domReady()\n    this.logDebugActivity(\"application\", \"starting\")\n    this.dispatcher.start()\n    this.router.start()\n    this.logDebugActivity(\"application\", \"start\")\n  }\n\n  stop() {\n    this.logDebugActivity(\"application\", \"stopping\")\n    this.dispatcher.stop()\n    this.router.stop()\n    this.logDebugActivity(\"application\", \"stop\")\n  }\n\n  register(identifier: string, controllerConstructor: ControllerConstructor) {\n    this.load({ identifier, controllerConstructor })\n  }\n\n  registerActionOption(name: string, filter: ActionDescriptorFilter) {\n    this.actionDescriptorFilters[name] = filter\n  }\n\n  load(...definitions: Definition[]): void\n  load(definitions: Definition[]): void\n  load(head: Definition | Definition[], ...rest: Definition[]) {\n    const definitions = Array.isArray(head) ? head : [head, ...rest]\n    definitions.forEach((definition) => {\n      if ((definition.controllerConstructor as any).shouldLoad) {\n        this.router.loadDefinition(definition)\n      }\n    })\n  }\n\n  unload(...identifiers: string[]): void\n  unload(identifiers: string[]): void\n  unload(head: string | string[], ...rest: string[]) {\n    const identifiers = Array.isArray(head) ? head : [head, ...rest]\n    identifiers.forEach((identifier) => this.router.unloadIdentifier(identifier))\n  }\n\n  // Controllers\n\n  get controllers(): Controller[] {\n    return this.router.contexts.map((context) => context.controller)\n  }\n\n  getControllerForElementAndIdentifier(element: Element, identifier: string): Controller | null {\n    const context = this.router.getContextForElementAndIdentifier(element, identifier)\n    return context ? context.controller : null\n  }\n\n  // Error handling\n\n  handleError(error: Error, message: string, detail: object) {\n    this.logger.error(`%s\\n\\n%o\\n\\n%o`, message, error, detail)\n\n    window.onerror?.(message, \"\", 0, 0, error)\n  }\n\n  // Debug logging\n\n  logDebugActivity = (identifier: string, functionName: string, detail: object = {}): void => {\n    if (this.debug) {\n      this.logFormattedMessage(identifier, functionName, detail)\n    }\n  }\n\n  private logFormattedMessage(identifier: string, functionName: string, detail: object = {}) {\n    detail = Object.assign({ application: this }, detail)\n\n    this.logger.groupCollapsed(`${identifier} #${functionName}`)\n    this.logger.log(\"details:\", { ...detail })\n    this.logger.groupEnd()\n  }\n}\n\nfunction domReady() {\n  return new Promise<void>((resolve) => {\n    if (document.readyState == \"loading\") {\n      document.addEventListener(\"DOMContentLoaded\", () => resolve())\n    } else {\n      resolve()\n    }\n  })\n}\n"
  },
  {
    "path": "src/core/binding.ts",
    "content": "import { Action } from \"./action\"\nimport { ActionEvent } from \"./action_event\"\nimport { Context } from \"./context\"\nimport { Controller } from \"./controller\"\nimport { Scope } from \"./scope\"\nexport class Binding {\n  readonly context: Context\n  readonly action: Action\n\n  constructor(context: Context, action: Action) {\n    this.context = context\n    this.action = action\n  }\n\n  get index(): number {\n    return this.action.index\n  }\n\n  get eventTarget(): EventTarget {\n    return this.action.eventTarget\n  }\n\n  get eventOptions(): AddEventListenerOptions {\n    return this.action.eventOptions\n  }\n\n  get identifier(): string {\n    return this.context.identifier\n  }\n\n  handleEvent(event: Event) {\n    const actionEvent = this.prepareActionEvent(event)\n    if (this.willBeInvokedByEvent(event) && this.applyEventModifiers(actionEvent)) {\n      this.invokeWithEvent(actionEvent)\n    }\n  }\n\n  get eventName(): string {\n    return this.action.eventName\n  }\n\n  get method(): Function {\n    const method = (this.controller as any)[this.methodName]\n    if (typeof method == \"function\") {\n      return method\n    }\n    throw new Error(`Action \"${this.action}\" references undefined method \"${this.methodName}\"`)\n  }\n\n  private applyEventModifiers(event: Event): boolean {\n    const { element } = this.action\n    const { actionDescriptorFilters } = this.context.application\n    const { controller } = this.context\n\n    let passes = true\n\n    for (const [name, value] of Object.entries(this.eventOptions)) {\n      if (name in actionDescriptorFilters) {\n        const filter = actionDescriptorFilters[name]\n\n        passes = passes && filter({ name, value, event, element, controller })\n      } else {\n        continue\n      }\n    }\n\n    return passes\n  }\n\n  private prepareActionEvent(event: Event): ActionEvent {\n    return Object.assign(event, { params: this.action.params })\n  }\n\n  private invokeWithEvent(event: ActionEvent) {\n    const { target, currentTarget } = event\n    try {\n      this.method.call(this.controller, event)\n      this.context.logDebugActivity(this.methodName, { event, target, currentTarget, action: this.methodName })\n    } catch (error: any) {\n      const { identifier, controller, element, index } = this\n      const detail = { identifier, controller, element, index, event }\n      this.context.handleError(error, `invoking action \"${this.action}\"`, detail)\n    }\n  }\n\n  private willBeInvokedByEvent(event: Event): boolean {\n    const eventTarget = event.target\n\n    if (event instanceof KeyboardEvent && this.action.shouldIgnoreKeyboardEvent(event)) {\n      return false\n    }\n\n    if (event instanceof MouseEvent && this.action.shouldIgnoreMouseEvent(event)) {\n      return false\n    }\n\n    if (this.element === eventTarget) {\n      return true\n    } else if (eventTarget instanceof Element && this.element.contains(eventTarget)) {\n      return this.scope.containsElement(eventTarget)\n    } else {\n      return this.scope.containsElement(this.action.element)\n    }\n  }\n\n  private get controller(): Controller {\n    return this.context.controller\n  }\n\n  private get methodName(): string {\n    return this.action.methodName\n  }\n\n  private get element(): Element {\n    return this.scope.element\n  }\n\n  private get scope(): Scope {\n    return this.context.scope\n  }\n}\n"
  },
  {
    "path": "src/core/binding_observer.ts",
    "content": "import { Action } from \"./action\"\nimport { Binding } from \"./binding\"\nimport { Context } from \"./context\"\nimport { ErrorHandler } from \"./error_handler\"\nimport { Schema } from \"./schema\"\nimport { Token, ValueListObserver, ValueListObserverDelegate } from \"../mutation-observers\"\n\nexport interface BindingObserverDelegate extends ErrorHandler {\n  bindingConnected(binding: Binding): void\n  bindingDisconnected(binding: Binding, clearEventListeners?: boolean): void\n}\n\nexport class BindingObserver implements ValueListObserverDelegate<Action> {\n  readonly context: Context\n  private delegate: BindingObserverDelegate\n  private valueListObserver?: ValueListObserver<Action>\n  private bindingsByAction: Map<Action, Binding>\n\n  constructor(context: Context, delegate: BindingObserverDelegate) {\n    this.context = context\n    this.delegate = delegate\n    this.bindingsByAction = new Map()\n  }\n\n  start() {\n    if (!this.valueListObserver) {\n      this.valueListObserver = new ValueListObserver(this.element, this.actionAttribute, this)\n      this.valueListObserver.start()\n    }\n  }\n\n  stop() {\n    if (this.valueListObserver) {\n      this.valueListObserver.stop()\n      delete this.valueListObserver\n      this.disconnectAllActions()\n    }\n  }\n\n  get element() {\n    return this.context.element\n  }\n\n  get identifier() {\n    return this.context.identifier\n  }\n\n  get actionAttribute() {\n    return this.schema.actionAttribute\n  }\n\n  get schema(): Schema {\n    return this.context.schema\n  }\n\n  get bindings(): Binding[] {\n    return Array.from(this.bindingsByAction.values())\n  }\n\n  private connectAction(action: Action) {\n    const binding = new Binding(this.context, action)\n    this.bindingsByAction.set(action, binding)\n    this.delegate.bindingConnected(binding)\n  }\n\n  private disconnectAction(action: Action) {\n    const binding = this.bindingsByAction.get(action)\n    if (binding) {\n      this.bindingsByAction.delete(action)\n      this.delegate.bindingDisconnected(binding)\n    }\n  }\n\n  private disconnectAllActions() {\n    this.bindings.forEach((binding) => this.delegate.bindingDisconnected(binding, true))\n    this.bindingsByAction.clear()\n  }\n\n  // Value observer delegate\n\n  parseValueForToken(token: Token): Action | undefined {\n    const action = Action.forToken(token, this.schema)\n    if (action.identifier == this.identifier) {\n      return action\n    }\n  }\n\n  elementMatchedValue(element: Element, action: Action) {\n    this.connectAction(action)\n  }\n\n  elementUnmatchedValue(element: Element, action: Action) {\n    this.disconnectAction(action)\n  }\n}\n"
  },
  {
    "path": "src/core/blessing.ts",
    "content": "import { Constructor } from \"./constructor\"\nimport { readInheritableStaticArrayValues } from \"./inheritable_statics\"\n\nexport type Blessing<T> = (constructor: Constructor<T>) => PropertyDescriptorMap\n\nexport interface Blessable<T> extends Constructor<T> {\n  readonly blessings?: Blessing<T>[]\n}\n\nexport function bless<T>(constructor: Blessable<T>): Constructor<T> {\n  return shadow(constructor, getBlessedProperties(constructor))\n}\n\nfunction shadow<T>(constructor: Constructor<T>, properties: PropertyDescriptorMap) {\n  const shadowConstructor = extend(constructor)\n  const shadowProperties = getShadowProperties(constructor.prototype, properties)\n  Object.defineProperties(shadowConstructor.prototype, shadowProperties)\n  return shadowConstructor\n}\n\nfunction getBlessedProperties<T>(constructor: Constructor<T>) {\n  const blessings = readInheritableStaticArrayValues(constructor, \"blessings\") as Blessing<T>[]\n  return blessings.reduce((blessedProperties, blessing) => {\n    const properties = blessing(constructor)\n    for (const key in properties) {\n      const descriptor = blessedProperties[key] || ({} as PropertyDescriptor)\n      blessedProperties[key] = Object.assign(descriptor, properties[key])\n    }\n    return blessedProperties\n  }, {} as PropertyDescriptorMap)\n}\n\nfunction getShadowProperties(prototype: any, properties: PropertyDescriptorMap) {\n  return getOwnKeys(properties).reduce((shadowProperties, key) => {\n    const descriptor = getShadowedDescriptor(prototype, properties, key)\n    if (descriptor) {\n      Object.assign(shadowProperties, { [key]: descriptor })\n    }\n    return shadowProperties\n  }, {} as PropertyDescriptorMap)\n}\n\nfunction getShadowedDescriptor(prototype: any, properties: PropertyDescriptorMap, key: string | symbol) {\n  const shadowingDescriptor = Object.getOwnPropertyDescriptor(prototype, key)\n  const shadowedByValue = shadowingDescriptor && \"value\" in shadowingDescriptor\n  if (!shadowedByValue) {\n    const descriptor = Object.getOwnPropertyDescriptor(properties, key)!.value\n    if (shadowingDescriptor) {\n      descriptor.get = shadowingDescriptor.get || descriptor.get\n      descriptor.set = shadowingDescriptor.set || descriptor.set\n    }\n    return descriptor\n  }\n}\n\nconst getOwnKeys = (() => {\n  if (typeof Object.getOwnPropertySymbols == \"function\") {\n    return (object: any) => [...Object.getOwnPropertyNames(object), ...Object.getOwnPropertySymbols(object)]\n  } else {\n    return Object.getOwnPropertyNames\n  }\n})()\n\nconst extend = (() => {\n  function extendWithReflect<T extends Constructor<any>>(constructor: T): T {\n    function extended() {\n      return Reflect.construct(constructor, arguments, new.target)\n    }\n\n    extended.prototype = Object.create(constructor.prototype, {\n      constructor: { value: extended },\n    })\n\n    Reflect.setPrototypeOf(extended, constructor)\n    return extended as any\n  }\n\n  function testReflectExtension() {\n    const a = function (this: any) {\n      this.a.call(this)\n    } as any\n    const b = extendWithReflect(a)\n    b.prototype.a = function () {}\n    return new b()\n  }\n\n  try {\n    testReflectExtension()\n    return extendWithReflect\n  } catch (error: any) {\n    return <T extends Constructor<any>>(constructor: T) => class extended extends constructor {}\n  }\n})()\n"
  },
  {
    "path": "src/core/class_map.ts",
    "content": "import { Scope } from \"./scope\"\nimport { tokenize } from \"./string_helpers\"\n\nexport class ClassMap {\n  readonly scope: Scope\n\n  constructor(scope: Scope) {\n    this.scope = scope\n  }\n\n  has(name: string) {\n    return this.data.has(this.getDataKey(name))\n  }\n\n  get(name: string): string | undefined {\n    return this.getAll(name)[0]\n  }\n\n  getAll(name: string) {\n    const tokenString = this.data.get(this.getDataKey(name)) || \"\"\n    return tokenize(tokenString)\n  }\n\n  getAttributeName(name: string) {\n    return this.data.getAttributeNameForKey(this.getDataKey(name))\n  }\n\n  getDataKey(name: string) {\n    return `${name}-class`\n  }\n\n  get data() {\n    return this.scope.data\n  }\n}\n"
  },
  {
    "path": "src/core/class_properties.ts",
    "content": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticArrayValues } from \"./inheritable_statics\"\nimport { capitalize } from \"./string_helpers\"\n\nexport function ClassPropertiesBlessing<T>(constructor: Constructor<T>) {\n  const classes = readInheritableStaticArrayValues(constructor, \"classes\")\n  return classes.reduce((properties, classDefinition) => {\n    return Object.assign(properties, propertiesForClassDefinition(classDefinition))\n  }, {} as PropertyDescriptorMap)\n}\n\nfunction propertiesForClassDefinition(key: string) {\n  return {\n    [`${key}Class`]: {\n      get(this: Controller) {\n        const { classes } = this\n        if (classes.has(key)) {\n          return classes.get(key)\n        } else {\n          const attribute = classes.getAttributeName(key)\n          throw new Error(`Missing attribute \"${attribute}\"`)\n        }\n      },\n    },\n\n    [`${key}Classes`]: {\n      get(this: Controller) {\n        return this.classes.getAll(key)\n      },\n    },\n\n    [`has${capitalize(key)}Class`]: {\n      get(this: Controller) {\n        return this.classes.has(key)\n      },\n    },\n  }\n}\n"
  },
  {
    "path": "src/core/constructor.ts",
    "content": "export type Constructor<T> = new (...args: any[]) => T\n"
  },
  {
    "path": "src/core/context.ts",
    "content": "import { Application } from \"./application\"\nimport { BindingObserver } from \"./binding_observer\"\nimport { Controller } from \"./controller\"\nimport { Dispatcher } from \"./dispatcher\"\nimport { ErrorHandler } from \"./error_handler\"\nimport { Module } from \"./module\"\nimport { Schema } from \"./schema\"\nimport { Scope } from \"./scope\"\nimport { ValueObserver } from \"./value_observer\"\nimport { TargetObserver, TargetObserverDelegate } from \"./target_observer\"\nimport { OutletObserver, OutletObserverDelegate } from \"./outlet_observer\"\nimport { namespaceCamelize } from \"./string_helpers\"\n\nexport class Context implements ErrorHandler, TargetObserverDelegate, OutletObserverDelegate {\n  readonly module: Module\n  readonly scope: Scope\n  readonly controller: Controller\n  private bindingObserver: BindingObserver\n  private valueObserver: ValueObserver\n  private targetObserver: TargetObserver\n  private outletObserver: OutletObserver\n\n  constructor(module: Module, scope: Scope) {\n    this.module = module\n    this.scope = scope\n    this.controller = new module.controllerConstructor(this)\n    this.bindingObserver = new BindingObserver(this, this.dispatcher)\n    this.valueObserver = new ValueObserver(this, this.controller)\n    this.targetObserver = new TargetObserver(this, this)\n    this.outletObserver = new OutletObserver(this, this)\n\n    try {\n      this.controller.initialize()\n      this.logDebugActivity(\"initialize\")\n    } catch (error: any) {\n      this.handleError(error, \"initializing controller\")\n    }\n  }\n\n  connect() {\n    this.bindingObserver.start()\n    this.valueObserver.start()\n    this.targetObserver.start()\n    this.outletObserver.start()\n\n    try {\n      this.controller.connect()\n      this.logDebugActivity(\"connect\")\n    } catch (error: any) {\n      this.handleError(error, \"connecting controller\")\n    }\n  }\n\n  refresh() {\n    this.outletObserver.refresh()\n  }\n\n  disconnect() {\n    try {\n      this.controller.disconnect()\n      this.logDebugActivity(\"disconnect\")\n    } catch (error: any) {\n      this.handleError(error, \"disconnecting controller\")\n    }\n\n    this.outletObserver.stop()\n    this.targetObserver.stop()\n    this.valueObserver.stop()\n    this.bindingObserver.stop()\n  }\n\n  get application(): Application {\n    return this.module.application\n  }\n\n  get identifier(): string {\n    return this.module.identifier\n  }\n\n  get schema(): Schema {\n    return this.application.schema\n  }\n\n  get dispatcher(): Dispatcher {\n    return this.application.dispatcher\n  }\n\n  get element(): Element {\n    return this.scope.element\n  }\n\n  get parentElement(): Element | null {\n    return this.element.parentElement\n  }\n\n  // Error handling\n\n  handleError(error: Error, message: string, detail: object = {}) {\n    const { identifier, controller, element } = this\n    detail = Object.assign({ identifier, controller, element }, detail)\n    this.application.handleError(error, `Error ${message}`, detail)\n  }\n\n  // Debug logging\n\n  logDebugActivity = (functionName: string, detail: object = {}): void => {\n    const { identifier, controller, element } = this\n    detail = Object.assign({ identifier, controller, element }, detail)\n    this.application.logDebugActivity(this.identifier, functionName, detail)\n  }\n\n  // Target observer delegate\n\n  targetConnected(element: Element, name: string) {\n    this.invokeControllerMethod(`${name}TargetConnected`, element)\n  }\n\n  targetDisconnected(element: Element, name: string) {\n    this.invokeControllerMethod(`${name}TargetDisconnected`, element)\n  }\n\n  // Outlet observer delegate\n\n  outletConnected(outlet: Controller, element: Element, name: string) {\n    this.invokeControllerMethod(`${namespaceCamelize(name)}OutletConnected`, outlet, element)\n  }\n\n  outletDisconnected(outlet: Controller, element: Element, name: string) {\n    this.invokeControllerMethod(`${namespaceCamelize(name)}OutletDisconnected`, outlet, element)\n  }\n\n  // Private\n\n  invokeControllerMethod(methodName: string, ...args: any[]) {\n    const controller: any = this.controller\n    if (typeof controller[methodName] == \"function\") {\n      controller[methodName](...args)\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/controller.ts",
    "content": "import { Application } from \"./application\"\nimport { ClassPropertiesBlessing } from \"./class_properties\"\nimport { Constructor } from \"./constructor\"\nimport { Context } from \"./context\"\nimport { OutletPropertiesBlessing } from \"./outlet_properties\"\nimport { TargetPropertiesBlessing } from \"./target_properties\"\nimport { ValuePropertiesBlessing, ValueDefinitionMap } from \"./value_properties\"\n\nexport type ControllerConstructor = Constructor<Controller>\n\ntype DispatchOptions = Partial<{\n  target: Element | Window | Document\n  detail: Object\n  prefix: string\n  bubbles: boolean\n  cancelable: boolean\n}>\n\nexport class Controller<ElementType extends Element = Element> {\n  static blessings = [\n    ClassPropertiesBlessing,\n    TargetPropertiesBlessing,\n    ValuePropertiesBlessing,\n    OutletPropertiesBlessing,\n  ]\n  static targets: string[] = []\n  static outlets: string[] = []\n  static values: ValueDefinitionMap = {}\n\n  static get shouldLoad() {\n    return true\n  }\n\n  static afterLoad(_identifier: string, _application: Application) {\n    return\n  }\n\n  readonly context: Context\n\n  constructor(context: Context) {\n    this.context = context\n  }\n\n  get application() {\n    return this.context.application\n  }\n\n  get scope() {\n    return this.context.scope\n  }\n\n  get element() {\n    return this.scope.element as ElementType\n  }\n\n  get identifier() {\n    return this.scope.identifier\n  }\n\n  get targets() {\n    return this.scope.targets\n  }\n\n  get outlets() {\n    return this.scope.outlets\n  }\n\n  get classes() {\n    return this.scope.classes\n  }\n\n  get data() {\n    return this.scope.data\n  }\n\n  initialize() {\n    // Override in your subclass to set up initial controller state\n  }\n\n  connect() {\n    // Override in your subclass to respond when the controller is connected to the DOM\n  }\n\n  disconnect() {\n    // Override in your subclass to respond when the controller is disconnected from the DOM\n  }\n\n  dispatch(\n    eventName: string,\n    {\n      target = this.element,\n      detail = {},\n      prefix = this.identifier,\n      bubbles = true,\n      cancelable = true,\n    }: DispatchOptions = {}\n  ) {\n    const type = prefix ? `${prefix}:${eventName}` : eventName\n    const event = new CustomEvent(type, { detail, bubbles, cancelable })\n    target.dispatchEvent(event)\n    return event\n  }\n}\n"
  },
  {
    "path": "src/core/data_map.ts",
    "content": "import { Scope } from \"./scope\"\nimport { dasherize } from \"./string_helpers\"\n\nexport class DataMap {\n  readonly scope: Scope\n\n  constructor(scope: Scope) {\n    this.scope = scope\n  }\n\n  get element(): Element {\n    return this.scope.element\n  }\n\n  get identifier(): string {\n    return this.scope.identifier\n  }\n\n  get(key: string): string | null {\n    const name = this.getAttributeNameForKey(key)\n    return this.element.getAttribute(name)\n  }\n\n  set(key: string, value: string): string | null {\n    const name = this.getAttributeNameForKey(key)\n    this.element.setAttribute(name, value)\n    return this.get(key)\n  }\n\n  has(key: string): boolean {\n    const name = this.getAttributeNameForKey(key)\n    return this.element.hasAttribute(name)\n  }\n\n  delete(key: string): boolean {\n    if (this.has(key)) {\n      const name = this.getAttributeNameForKey(key)\n      this.element.removeAttribute(name)\n      return true\n    } else {\n      return false\n    }\n  }\n\n  getAttributeNameForKey(key: string): string {\n    return `data-${this.identifier}-${dasherize(key)}`\n  }\n}\n"
  },
  {
    "path": "src/core/definition.ts",
    "content": "import { bless } from \"./blessing\"\nimport { ControllerConstructor } from \"./controller\"\n\nexport interface Definition {\n  identifier: string\n  controllerConstructor: ControllerConstructor\n}\n\nexport function blessDefinition(definition: Definition): Definition {\n  return {\n    identifier: definition.identifier,\n    controllerConstructor: bless(definition.controllerConstructor),\n  }\n}\n"
  },
  {
    "path": "src/core/dispatcher.ts",
    "content": "import { Application } from \"./application\"\nimport { Binding } from \"./binding\"\nimport { BindingObserverDelegate } from \"./binding_observer\"\nimport { EventListener } from \"./event_listener\"\n\nexport class Dispatcher implements BindingObserverDelegate {\n  readonly application: Application\n  private eventListenerMaps: Map<EventTarget, Map<string, EventListener>>\n  private started: boolean\n\n  constructor(application: Application) {\n    this.application = application\n    this.eventListenerMaps = new Map()\n    this.started = false\n  }\n\n  start() {\n    if (!this.started) {\n      this.started = true\n      this.eventListeners.forEach((eventListener) => eventListener.connect())\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      this.started = false\n      this.eventListeners.forEach((eventListener) => eventListener.disconnect())\n    }\n  }\n\n  get eventListeners(): EventListener[] {\n    return Array.from(this.eventListenerMaps.values()).reduce(\n      (listeners, map) => listeners.concat(Array.from(map.values())),\n      [] as EventListener[]\n    )\n  }\n\n  // Binding observer delegate\n\n  bindingConnected(binding: Binding) {\n    this.fetchEventListenerForBinding(binding).bindingConnected(binding)\n  }\n\n  bindingDisconnected(binding: Binding, clearEventListeners = false) {\n    this.fetchEventListenerForBinding(binding).bindingDisconnected(binding)\n    if (clearEventListeners) this.clearEventListenersForBinding(binding)\n  }\n\n  // Error handling\n\n  handleError(error: Error, message: string, detail: object = {}) {\n    this.application.handleError(error, `Error ${message}`, detail)\n  }\n\n  private clearEventListenersForBinding(binding: Binding) {\n    const eventListener = this.fetchEventListenerForBinding(binding)\n    if (!eventListener.hasBindings()) {\n      eventListener.disconnect()\n      this.removeMappedEventListenerFor(binding)\n    }\n  }\n\n  private removeMappedEventListenerFor(binding: Binding) {\n    const { eventTarget, eventName, eventOptions } = binding\n    const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget)\n    const cacheKey = this.cacheKey(eventName, eventOptions)\n\n    eventListenerMap.delete(cacheKey)\n    if (eventListenerMap.size == 0) this.eventListenerMaps.delete(eventTarget)\n  }\n\n  private fetchEventListenerForBinding(binding: Binding): EventListener {\n    const { eventTarget, eventName, eventOptions } = binding\n    return this.fetchEventListener(eventTarget, eventName, eventOptions)\n  }\n\n  private fetchEventListener(\n    eventTarget: EventTarget,\n    eventName: string,\n    eventOptions: AddEventListenerOptions\n  ): EventListener {\n    const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget)\n    const cacheKey = this.cacheKey(eventName, eventOptions)\n    let eventListener = eventListenerMap.get(cacheKey)\n    if (!eventListener) {\n      eventListener = this.createEventListener(eventTarget, eventName, eventOptions)\n      eventListenerMap.set(cacheKey, eventListener)\n    }\n    return eventListener\n  }\n\n  private createEventListener(\n    eventTarget: EventTarget,\n    eventName: string,\n    eventOptions: AddEventListenerOptions\n  ): EventListener {\n    const eventListener = new EventListener(eventTarget, eventName, eventOptions)\n    if (this.started) {\n      eventListener.connect()\n    }\n    return eventListener\n  }\n\n  private fetchEventListenerMapForEventTarget(eventTarget: EventTarget): Map<string, EventListener> {\n    let eventListenerMap = this.eventListenerMaps.get(eventTarget)\n    if (!eventListenerMap) {\n      eventListenerMap = new Map()\n      this.eventListenerMaps.set(eventTarget, eventListenerMap)\n    }\n    return eventListenerMap\n  }\n\n  private cacheKey(eventName: string, eventOptions: any): string {\n    const parts = [eventName]\n    Object.keys(eventOptions)\n      .sort()\n      .forEach((key) => {\n        parts.push(`${eventOptions[key] ? \"\" : \"!\"}${key}`)\n      })\n    return parts.join(\":\")\n  }\n}\n"
  },
  {
    "path": "src/core/error_handler.ts",
    "content": "export interface ErrorHandler {\n  handleError(error: Error, message: string, detail: object): void\n}\n"
  },
  {
    "path": "src/core/event_listener.ts",
    "content": "import { Binding } from \"./binding\"\n\nexport class EventListener implements EventListenerObject {\n  readonly eventTarget: EventTarget\n  readonly eventName: string\n  readonly eventOptions: AddEventListenerOptions\n  private unorderedBindings: Set<Binding>\n\n  constructor(eventTarget: EventTarget, eventName: string, eventOptions: AddEventListenerOptions) {\n    this.eventTarget = eventTarget\n    this.eventName = eventName\n    this.eventOptions = eventOptions\n    this.unorderedBindings = new Set()\n  }\n\n  connect() {\n    this.eventTarget.addEventListener(this.eventName, this, this.eventOptions)\n  }\n\n  disconnect() {\n    this.eventTarget.removeEventListener(this.eventName, this, this.eventOptions)\n  }\n\n  // Binding observer delegate\n\n  bindingConnected(binding: Binding) {\n    this.unorderedBindings.add(binding)\n  }\n\n  bindingDisconnected(binding: Binding) {\n    this.unorderedBindings.delete(binding)\n  }\n\n  handleEvent(event: Event) {\n    // FIXME: Determine why TS won't recognize that the extended event has immediatePropagationStopped\n    const extendedEvent = extendEvent(event) as any\n    for (const binding of this.bindings) {\n      if (extendedEvent.immediatePropagationStopped) {\n        break\n      } else {\n        binding.handleEvent(extendedEvent)\n      }\n    }\n  }\n\n  hasBindings() {\n    return this.unorderedBindings.size > 0\n  }\n\n  get bindings(): Binding[] {\n    return Array.from(this.unorderedBindings).sort((left, right) => {\n      const leftIndex = left.index,\n        rightIndex = right.index\n      return leftIndex < rightIndex ? -1 : leftIndex > rightIndex ? 1 : 0\n    })\n  }\n}\n\nfunction extendEvent(event: Event) {\n  if (\"immediatePropagationStopped\" in event) {\n    return event\n  } else {\n    const { stopImmediatePropagation } = event\n    return Object.assign(event, {\n      immediatePropagationStopped: false,\n      stopImmediatePropagation() {\n        this.immediatePropagationStopped = true\n        stopImmediatePropagation.call(this)\n      },\n    })\n  }\n}\n"
  },
  {
    "path": "src/core/guide.ts",
    "content": "import { Logger } from \"./logger\"\n\nexport class Guide {\n  readonly logger: Logger\n  readonly warnedKeysByObject: WeakMap<any, Set<string>> = new WeakMap()\n\n  constructor(logger: Logger) {\n    this.logger = logger\n  }\n\n  warn(object: any, key: string, message: string) {\n    let warnedKeys: Set<string> | undefined = this.warnedKeysByObject.get(object)\n\n    if (!warnedKeys) {\n      warnedKeys = new Set()\n      this.warnedKeysByObject.set(object, warnedKeys)\n    }\n\n    if (!warnedKeys.has(key)) {\n      warnedKeys.add(key)\n      this.logger.warn(message, object)\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/index.ts",
    "content": "export { ActionEvent } from \"./action_event\"\nexport { Application } from \"./application\"\nexport { Context } from \"./context\"\nexport { Controller, ControllerConstructor } from \"./controller\"\nexport { Definition } from \"./definition\"\nexport { Schema, defaultSchema } from \"./schema\"\n"
  },
  {
    "path": "src/core/inheritable_statics.ts",
    "content": "import { Constructor } from \"./constructor\"\n\nexport function readInheritableStaticArrayValues<T, U = string>(constructor: Constructor<T>, propertyName: string) {\n  const ancestors = getAncestorsForConstructor(constructor)\n  return Array.from(\n    ancestors.reduce((values, constructor) => {\n      getOwnStaticArrayValues(constructor, propertyName).forEach((name) => values.add(name))\n      return values\n    }, new Set() as Set<U>)\n  )\n}\n\nexport function readInheritableStaticObjectPairs<T, U>(constructor: Constructor<T>, propertyName: string) {\n  const ancestors = getAncestorsForConstructor(constructor)\n  return ancestors.reduce((pairs, constructor) => {\n    pairs.push(...(getOwnStaticObjectPairs(constructor, propertyName) as any))\n    return pairs\n  }, [] as [string, U][])\n}\n\nfunction getAncestorsForConstructor<T>(constructor: Constructor<T>) {\n  const ancestors: Constructor<any>[] = []\n  while (constructor) {\n    ancestors.push(constructor)\n    constructor = Object.getPrototypeOf(constructor)\n  }\n  return ancestors.reverse()\n}\n\nfunction getOwnStaticArrayValues<T>(constructor: Constructor<T>, propertyName: string) {\n  const definition = (constructor as any)[propertyName]\n  return Array.isArray(definition) ? definition : []\n}\n\nfunction getOwnStaticObjectPairs<T, U>(constructor: Constructor<T>, propertyName: string) {\n  const definition = (constructor as any)[propertyName]\n  return definition ? Object.keys(definition).map((key) => [key, definition[key]] as [string, U]) : []\n}\n"
  },
  {
    "path": "src/core/logger.ts",
    "content": "export interface Logger {\n  log(message: string, ...args: any[]): void\n  warn(message: string, ...args: any[]): void\n  error(message: string, ...args: any[]): void\n  groupCollapsed(groupTitle?: string, ...args: any[]): void\n  groupEnd(): void\n}\n"
  },
  {
    "path": "src/core/module.ts",
    "content": "import { Application } from \"./application\"\nimport { Context } from \"./context\"\nimport { ControllerConstructor } from \"./controller\"\nimport { Definition, blessDefinition } from \"./definition\"\nimport { Scope } from \"./scope\"\n\nexport class Module {\n  readonly application: Application\n  readonly definition: Definition\n  private contextsByScope: WeakMap<Scope, Context>\n  private connectedContexts: Set<Context>\n\n  constructor(application: Application, definition: Definition) {\n    this.application = application\n    this.definition = blessDefinition(definition)\n    this.contextsByScope = new WeakMap()\n    this.connectedContexts = new Set()\n  }\n\n  get identifier(): string {\n    return this.definition.identifier\n  }\n\n  get controllerConstructor(): ControllerConstructor {\n    return this.definition.controllerConstructor\n  }\n\n  get contexts(): Context[] {\n    return Array.from(this.connectedContexts)\n  }\n\n  connectContextForScope(scope: Scope) {\n    const context = this.fetchContextForScope(scope)\n    this.connectedContexts.add(context)\n    context.connect()\n  }\n\n  disconnectContextForScope(scope: Scope) {\n    const context = this.contextsByScope.get(scope)\n    if (context) {\n      this.connectedContexts.delete(context)\n      context.disconnect()\n    }\n  }\n\n  private fetchContextForScope(scope: Scope): Context {\n    let context = this.contextsByScope.get(scope)\n    if (!context) {\n      context = new Context(this, scope)\n      this.contextsByScope.set(scope, context)\n    }\n    return context\n  }\n}\n"
  },
  {
    "path": "src/core/outlet_observer.ts",
    "content": "import { Multimap } from \"../multimap\"\nimport { AttributeObserver, AttributeObserverDelegate } from \"../mutation-observers\"\nimport { SelectorObserver, SelectorObserverDelegate } from \"../mutation-observers\"\nimport { Context } from \"./context\"\nimport { Controller } from \"./controller\"\n\nimport { readInheritableStaticArrayValues } from \"./inheritable_statics\"\n\ntype OutletObserverDetails = { outletName: string }\n\nexport interface OutletObserverDelegate {\n  outletConnected(outlet: Controller, element: Element, outletName: string): void\n  outletDisconnected(outlet: Controller, element: Element, outletName: string): void\n}\n\nexport class OutletObserver implements AttributeObserverDelegate, SelectorObserverDelegate {\n  started: boolean\n  readonly context: Context\n  readonly delegate: OutletObserverDelegate\n  readonly outletsByName: Multimap<string, Controller>\n  readonly outletElementsByName: Multimap<string, Element>\n  private selectorObserverMap: Map<string, SelectorObserver>\n  private attributeObserverMap: Map<string, AttributeObserver>\n\n  constructor(context: Context, delegate: OutletObserverDelegate) {\n    this.started = false\n    this.context = context\n    this.delegate = delegate\n    this.outletsByName = new Multimap()\n    this.outletElementsByName = new Multimap()\n    this.selectorObserverMap = new Map()\n    this.attributeObserverMap = new Map()\n  }\n\n  start() {\n    if (!this.started) {\n      this.outletDefinitions.forEach((outletName) => {\n        this.setupSelectorObserverForOutlet(outletName)\n        this.setupAttributeObserverForOutlet(outletName)\n      })\n      this.started = true\n      this.dependentContexts.forEach((context) => context.refresh())\n    }\n  }\n\n  refresh() {\n    this.selectorObserverMap.forEach((observer) => observer.refresh())\n    this.attributeObserverMap.forEach((observer) => observer.refresh())\n  }\n\n  stop() {\n    if (this.started) {\n      this.started = false\n      this.disconnectAllOutlets()\n      this.stopSelectorObservers()\n      this.stopAttributeObservers()\n    }\n  }\n\n  stopSelectorObservers() {\n    if (this.selectorObserverMap.size > 0) {\n      this.selectorObserverMap.forEach((observer) => observer.stop())\n      this.selectorObserverMap.clear()\n    }\n  }\n\n  stopAttributeObservers() {\n    if (this.attributeObserverMap.size > 0) {\n      this.attributeObserverMap.forEach((observer) => observer.stop())\n      this.attributeObserverMap.clear()\n    }\n  }\n\n  // Selector observer delegate\n\n  selectorMatched(element: Element, _selector: string, { outletName }: OutletObserverDetails) {\n    const outlet = this.getOutlet(element, outletName)\n\n    if (outlet) {\n      this.connectOutlet(outlet, element, outletName)\n    }\n  }\n\n  selectorUnmatched(element: Element, _selector: string, { outletName }: OutletObserverDetails) {\n    const outlet = this.getOutletFromMap(element, outletName)\n\n    if (outlet) {\n      this.disconnectOutlet(outlet, element, outletName)\n    }\n  }\n\n  selectorMatchElement(element: Element, { outletName }: OutletObserverDetails) {\n    const selector = this.selector(outletName)\n    const hasOutlet = this.hasOutlet(element, outletName)\n    const hasOutletController = element.matches(`[${this.schema.controllerAttribute}~=${outletName}]`)\n\n    if (selector) {\n      return hasOutlet && hasOutletController && element.matches(selector)\n    } else {\n      return false\n    }\n  }\n\n  // Attribute observer delegate\n\n  elementMatchedAttribute(_element: Element, attributeName: string) {\n    const outletName = this.getOutletNameFromOutletAttributeName(attributeName)\n\n    if (outletName) {\n      this.updateSelectorObserverForOutlet(outletName)\n    }\n  }\n\n  elementAttributeValueChanged(_element: Element, attributeName: string) {\n    const outletName = this.getOutletNameFromOutletAttributeName(attributeName)\n\n    if (outletName) {\n      this.updateSelectorObserverForOutlet(outletName)\n    }\n  }\n\n  elementUnmatchedAttribute(_element: Element, attributeName: string) {\n    const outletName = this.getOutletNameFromOutletAttributeName(attributeName)\n\n    if (outletName) {\n      this.updateSelectorObserverForOutlet(outletName)\n    }\n  }\n\n  // Outlet management\n\n  connectOutlet(outlet: Controller, element: Element, outletName: string) {\n    if (!this.outletElementsByName.has(outletName, element)) {\n      this.outletsByName.add(outletName, outlet)\n      this.outletElementsByName.add(outletName, element)\n      this.selectorObserverMap.get(outletName)?.pause(() => this.delegate.outletConnected(outlet, element, outletName))\n    }\n  }\n\n  disconnectOutlet(outlet: Controller, element: Element, outletName: string) {\n    if (this.outletElementsByName.has(outletName, element)) {\n      this.outletsByName.delete(outletName, outlet)\n      this.outletElementsByName.delete(outletName, element)\n      this.selectorObserverMap\n        .get(outletName)\n        ?.pause(() => this.delegate.outletDisconnected(outlet, element, outletName))\n    }\n  }\n\n  disconnectAllOutlets() {\n    for (const outletName of this.outletElementsByName.keys) {\n      for (const element of this.outletElementsByName.getValuesForKey(outletName)) {\n        for (const outlet of this.outletsByName.getValuesForKey(outletName)) {\n          this.disconnectOutlet(outlet, element, outletName)\n        }\n      }\n    }\n  }\n\n  // Observer management\n\n  private updateSelectorObserverForOutlet(outletName: string) {\n    const observer = this.selectorObserverMap.get(outletName)\n\n    if (observer) {\n      observer.selector = this.selector(outletName)\n    }\n  }\n\n  private setupSelectorObserverForOutlet(outletName: string) {\n    const selector = this.selector(outletName)\n    const selectorObserver = new SelectorObserver(document.body, selector!, this, { outletName })\n\n    this.selectorObserverMap.set(outletName, selectorObserver)\n\n    selectorObserver.start()\n  }\n\n  private setupAttributeObserverForOutlet(outletName: string) {\n    const attributeName = this.attributeNameForOutletName(outletName)\n    const attributeObserver = new AttributeObserver(this.scope.element, attributeName, this)\n\n    this.attributeObserverMap.set(outletName, attributeObserver)\n\n    attributeObserver.start()\n  }\n\n  // Private\n\n  private selector(outletName: string) {\n    return this.scope.outlets.getSelectorForOutletName(outletName)\n  }\n\n  private attributeNameForOutletName(outletName: string) {\n    return this.scope.schema.outletAttributeForScope(this.identifier, outletName)\n  }\n\n  private getOutletNameFromOutletAttributeName(attributeName: string) {\n    return this.outletDefinitions.find((outletName) => this.attributeNameForOutletName(outletName) === attributeName)\n  }\n\n  private get outletDependencies() {\n    const dependencies = new Multimap<string, string>()\n\n    this.router.modules.forEach((module) => {\n      const constructor = module.definition.controllerConstructor\n      const outlets = readInheritableStaticArrayValues(constructor, \"outlets\")\n\n      outlets.forEach((outlet) => dependencies.add(outlet, module.identifier))\n    })\n\n    return dependencies\n  }\n\n  private get outletDefinitions() {\n    return this.outletDependencies.getKeysForValue(this.identifier)\n  }\n\n  private get dependentControllerIdentifiers() {\n    return this.outletDependencies.getValuesForKey(this.identifier)\n  }\n\n  private get dependentContexts() {\n    const identifiers = this.dependentControllerIdentifiers\n    return this.router.contexts.filter((context) => identifiers.includes(context.identifier))\n  }\n\n  private hasOutlet(element: Element, outletName: string) {\n    return !!this.getOutlet(element, outletName) || !!this.getOutletFromMap(element, outletName)\n  }\n\n  private getOutlet(element: Element, outletName: string) {\n    return this.application.getControllerForElementAndIdentifier(element, outletName)\n  }\n\n  private getOutletFromMap(element: Element, outletName: string) {\n    return this.outletsByName.getValuesForKey(outletName).find((outlet) => outlet.element === element)\n  }\n\n  private get scope() {\n    return this.context.scope\n  }\n\n  private get schema() {\n    return this.context.schema\n  }\n\n  private get identifier() {\n    return this.context.identifier\n  }\n\n  private get application() {\n    return this.context.application\n  }\n\n  private get router() {\n    return this.application.router\n  }\n}\n"
  },
  {
    "path": "src/core/outlet_properties.ts",
    "content": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticArrayValues } from \"./inheritable_statics\"\nimport { capitalize, namespaceCamelize } from \"./string_helpers\"\n\nexport function OutletPropertiesBlessing<T>(constructor: Constructor<T>) {\n  const outlets = readInheritableStaticArrayValues(constructor, \"outlets\")\n  return outlets.reduce((properties: any, outletDefinition: any) => {\n    return Object.assign(properties, propertiesForOutletDefinition(outletDefinition))\n  }, {} as PropertyDescriptorMap)\n}\n\nfunction getOutletController(controller: Controller, element: Element, identifier: string) {\n  return controller.application.getControllerForElementAndIdentifier(element, identifier)\n}\n\nfunction getControllerAndEnsureConnectedScope(controller: Controller, element: Element, outletName: string) {\n  let outletController = getOutletController(controller, element, outletName)\n  if (outletController) return outletController\n\n  controller.application.router.proposeToConnectScopeForElementAndIdentifier(element, outletName)\n\n  outletController = getOutletController(controller, element, outletName)\n  if (outletController) return outletController\n}\n\nfunction propertiesForOutletDefinition(name: string) {\n  const camelizedName = namespaceCamelize(name)\n\n  return {\n    [`${camelizedName}Outlet`]: {\n      get(this: Controller) {\n        const outletElement = this.outlets.find(name)\n        const selector = this.outlets.getSelectorForOutletName(name)\n\n        if (outletElement) {\n          const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name)\n\n          if (outletController) return outletController\n\n          throw new Error(\n            `The provided outlet element is missing an outlet controller \"${name}\" instance for host controller \"${this.identifier}\"`\n          )\n        }\n\n        throw new Error(\n          `Missing outlet element \"${name}\" for host controller \"${this.identifier}\". Stimulus couldn't find a matching outlet element using selector \"${selector}\".`\n        )\n      },\n    },\n\n    [`${camelizedName}Outlets`]: {\n      get(this: Controller) {\n        const outlets = this.outlets.findAll(name)\n\n        if (outlets.length > 0) {\n          return outlets\n            .map((outletElement: Element) => {\n              const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name)\n\n              if (outletController) return outletController\n\n              console.warn(\n                `The provided outlet element is missing an outlet controller \"${name}\" instance for host controller \"${this.identifier}\"`,\n                outletElement\n              )\n            })\n            .filter((controller) => controller) as Controller[]\n        }\n\n        return []\n      },\n    },\n\n    [`${camelizedName}OutletElement`]: {\n      get(this: Controller) {\n        const outletElement = this.outlets.find(name)\n        const selector = this.outlets.getSelectorForOutletName(name)\n\n        if (outletElement) {\n          return outletElement\n        } else {\n          throw new Error(\n            `Missing outlet element \"${name}\" for host controller \"${this.identifier}\". Stimulus couldn't find a matching outlet element using selector \"${selector}\".`\n          )\n        }\n      },\n    },\n\n    [`${camelizedName}OutletElements`]: {\n      get(this: Controller) {\n        return this.outlets.findAll(name)\n      },\n    },\n\n    [`has${capitalize(camelizedName)}Outlet`]: {\n      get(this: Controller) {\n        return this.outlets.has(name)\n      },\n    },\n  }\n}\n"
  },
  {
    "path": "src/core/outlet_set.ts",
    "content": "import { Scope } from \"./scope\"\n\nexport class OutletSet {\n  readonly scope: Scope\n  readonly controllerElement: Element\n\n  constructor(scope: Scope, controllerElement: Element) {\n    this.scope = scope\n    this.controllerElement = controllerElement\n  }\n\n  get element() {\n    return this.scope.element\n  }\n\n  get identifier() {\n    return this.scope.identifier\n  }\n\n  get schema() {\n    return this.scope.schema\n  }\n\n  has(outletName: string) {\n    return this.find(outletName) != null\n  }\n\n  find(...outletNames: string[]) {\n    return outletNames.reduce(\n      (outlet, outletName) => outlet || this.findOutlet(outletName),\n      undefined as Element | undefined\n    )\n  }\n\n  findAll(...outletNames: string[]) {\n    return outletNames.reduce(\n      (outlets, outletName) => [...outlets, ...this.findAllOutlets(outletName)],\n      [] as Element[]\n    )\n  }\n\n  getSelectorForOutletName(outletName: string) {\n    const attributeName = this.schema.outletAttributeForScope(this.identifier, outletName)\n    return this.controllerElement.getAttribute(attributeName)\n  }\n\n  private findOutlet(outletName: string) {\n    const selector = this.getSelectorForOutletName(outletName)\n    if (selector) return this.findElement(selector, outletName)\n  }\n\n  private findAllOutlets(outletName: string) {\n    const selector = this.getSelectorForOutletName(outletName)\n    return selector ? this.findAllElements(selector, outletName) : []\n  }\n\n  private findElement(selector: string, outletName: string): Element | undefined {\n    const elements = this.scope.queryElements(selector)\n    return elements.filter((element) => this.matchesElement(element, selector, outletName))[0]\n  }\n\n  private findAllElements(selector: string, outletName: string): Element[] {\n    const elements = this.scope.queryElements(selector)\n    return elements.filter((element) => this.matchesElement(element, selector, outletName))\n  }\n\n  private matchesElement(element: Element, selector: string, outletName: string): boolean {\n    const controllerAttribute = element.getAttribute(this.scope.schema.controllerAttribute) || \"\"\n    return element.matches(selector) && controllerAttribute.split(\" \").includes(outletName)\n  }\n}\n"
  },
  {
    "path": "src/core/router.ts",
    "content": "import { Application } from \"./application\"\nimport { Context } from \"./context\"\nimport { Definition } from \"./definition\"\nimport { Module } from \"./module\"\nimport { Multimap } from \"../multimap\"\nimport { Scope } from \"./scope\"\nimport { ScopeObserver, ScopeObserverDelegate } from \"./scope_observer\"\n\nexport class Router implements ScopeObserverDelegate {\n  readonly application: Application\n  private scopeObserver: ScopeObserver\n  private scopesByIdentifier: Multimap<string, Scope>\n  private modulesByIdentifier: Map<string, Module>\n\n  constructor(application: Application) {\n    this.application = application\n    this.scopeObserver = new ScopeObserver(this.element, this.schema, this)\n    this.scopesByIdentifier = new Multimap()\n    this.modulesByIdentifier = new Map()\n  }\n\n  get element() {\n    return this.application.element\n  }\n\n  get schema() {\n    return this.application.schema\n  }\n\n  get logger() {\n    return this.application.logger\n  }\n\n  get controllerAttribute(): string {\n    return this.schema.controllerAttribute\n  }\n\n  get modules() {\n    return Array.from(this.modulesByIdentifier.values())\n  }\n\n  get contexts() {\n    return this.modules.reduce((contexts, module) => contexts.concat(module.contexts), [] as Context[])\n  }\n\n  start() {\n    this.scopeObserver.start()\n  }\n\n  stop() {\n    this.scopeObserver.stop()\n  }\n\n  loadDefinition(definition: Definition) {\n    this.unloadIdentifier(definition.identifier)\n    const module = new Module(this.application, definition)\n    this.connectModule(module)\n    const afterLoad = (definition.controllerConstructor as any).afterLoad\n    if (afterLoad) {\n      afterLoad.call(definition.controllerConstructor, definition.identifier, this.application)\n    }\n  }\n\n  unloadIdentifier(identifier: string) {\n    const module = this.modulesByIdentifier.get(identifier)\n    if (module) {\n      this.disconnectModule(module)\n    }\n  }\n\n  getContextForElementAndIdentifier(element: Element, identifier: string) {\n    const module = this.modulesByIdentifier.get(identifier)\n    if (module) {\n      return module.contexts.find((context) => context.element == element)\n    }\n  }\n\n  proposeToConnectScopeForElementAndIdentifier(element: Element, identifier: string) {\n    const scope = this.scopeObserver.parseValueForElementAndIdentifier(element, identifier)\n\n    if (scope) {\n      this.scopeObserver.elementMatchedValue(scope.element, scope)\n    } else {\n      console.error(`Couldn't find or create scope for identifier: \"${identifier}\" and element:`, element)\n    }\n  }\n\n  // Error handler delegate\n\n  handleError(error: Error, message: string, detail: any) {\n    this.application.handleError(error, message, detail)\n  }\n\n  // Scope observer delegate\n\n  createScopeForElementAndIdentifier(element: Element, identifier: string) {\n    return new Scope(this.schema, element, identifier, this.logger)\n  }\n\n  scopeConnected(scope: Scope) {\n    this.scopesByIdentifier.add(scope.identifier, scope)\n    const module = this.modulesByIdentifier.get(scope.identifier)\n    if (module) {\n      module.connectContextForScope(scope)\n    }\n  }\n\n  scopeDisconnected(scope: Scope) {\n    this.scopesByIdentifier.delete(scope.identifier, scope)\n    const module = this.modulesByIdentifier.get(scope.identifier)\n    if (module) {\n      module.disconnectContextForScope(scope)\n    }\n  }\n\n  // Modules\n\n  private connectModule(module: Module) {\n    this.modulesByIdentifier.set(module.identifier, module)\n    const scopes = this.scopesByIdentifier.getValuesForKey(module.identifier)\n    scopes.forEach((scope) => module.connectContextForScope(scope))\n  }\n\n  private disconnectModule(module: Module) {\n    this.modulesByIdentifier.delete(module.identifier)\n    const scopes = this.scopesByIdentifier.getValuesForKey(module.identifier)\n    scopes.forEach((scope) => module.disconnectContextForScope(scope))\n  }\n}\n"
  },
  {
    "path": "src/core/schema.ts",
    "content": "export interface Schema {\n  controllerAttribute: string\n  actionAttribute: string\n  targetAttribute: string\n  targetAttributeForScope(identifier: string): string\n  outletAttributeForScope(identifier: string, outlet: string): string\n  keyMappings: { [key: string]: string }\n}\n\nexport const defaultSchema: Schema = {\n  controllerAttribute: \"data-controller\",\n  actionAttribute: \"data-action\",\n  targetAttribute: \"data-target\",\n  targetAttributeForScope: (identifier) => `data-${identifier}-target`,\n  outletAttributeForScope: (identifier, outlet) => `data-${identifier}-${outlet}-outlet`,\n  keyMappings: {\n    enter: \"Enter\",\n    tab: \"Tab\",\n    esc: \"Escape\",\n    space: \" \",\n    up: \"ArrowUp\",\n    down: \"ArrowDown\",\n    left: \"ArrowLeft\",\n    right: \"ArrowRight\",\n    home: \"Home\",\n    end: \"End\",\n    page_up: \"PageUp\",\n    page_down: \"PageDown\",\n    // [a-z]\n    ...objectFromEntries(\"abcdefghijklmnopqrstuvwxyz\".split(\"\").map((c) => [c, c])),\n    // [0-9]\n    ...objectFromEntries(\"0123456789\".split(\"\").map((n) => [n, n])),\n  },\n}\n\nfunction objectFromEntries(array: [string, any][]): object {\n  // polyfill\n  return array.reduce((memo, [k, v]) => ({ ...memo, [k]: v }), {})\n}\n"
  },
  {
    "path": "src/core/scope.ts",
    "content": "import { ClassMap } from \"./class_map\"\nimport { DataMap } from \"./data_map\"\nimport { Guide } from \"./guide\"\nimport { Logger } from \"./logger\"\nimport { Schema } from \"./schema\"\nimport { attributeValueContainsToken } from \"./selectors\"\nimport { TargetSet } from \"./target_set\"\nimport { OutletSet } from \"./outlet_set\"\n\nexport class Scope {\n  readonly schema: Schema\n  readonly element: Element\n  readonly identifier: string\n  readonly guide: Guide\n  readonly outlets: OutletSet\n  readonly targets = new TargetSet(this)\n  readonly classes = new ClassMap(this)\n  readonly data = new DataMap(this)\n\n  constructor(schema: Schema, element: Element, identifier: string, logger: Logger) {\n    this.schema = schema\n    this.element = element\n    this.identifier = identifier\n    this.guide = new Guide(logger)\n    this.outlets = new OutletSet(this.documentScope, element)\n  }\n\n  findElement(selector: string): Element | undefined {\n    return this.element.matches(selector) ? this.element : this.queryElements(selector).find(this.containsElement)\n  }\n\n  findAllElements(selector: string): Element[] {\n    return [\n      ...(this.element.matches(selector) ? [this.element] : []),\n      ...this.queryElements(selector).filter(this.containsElement),\n    ]\n  }\n\n  containsElement = (element: Element): boolean => {\n    return element.closest(this.controllerSelector) === this.element\n  }\n\n  queryElements(selector: string): Element[] {\n    return Array.from(this.element.querySelectorAll(selector))\n  }\n\n  private get controllerSelector(): string {\n    return attributeValueContainsToken(this.schema.controllerAttribute, this.identifier)\n  }\n\n  private get isDocumentScope() {\n    return this.element === document.documentElement\n  }\n\n  private get documentScope(): Scope {\n    return this.isDocumentScope\n      ? this\n      : new Scope(this.schema, document.documentElement, this.identifier, this.guide.logger)\n  }\n}\n"
  },
  {
    "path": "src/core/scope_observer.ts",
    "content": "import { ErrorHandler } from \"./error_handler\"\nimport { Schema } from \"./schema\"\nimport { Scope } from \"./scope\"\nimport { Token, ValueListObserver, ValueListObserverDelegate } from \"../mutation-observers\"\n\nexport interface ScopeObserverDelegate extends ErrorHandler {\n  createScopeForElementAndIdentifier(element: Element, identifier: string): Scope\n  scopeConnected(scope: Scope): void\n  scopeDisconnected(scope: Scope): void\n}\n\nexport class ScopeObserver implements ValueListObserverDelegate<Scope> {\n  readonly element: Element\n  readonly schema: Schema\n  private delegate: ScopeObserverDelegate\n  private valueListObserver: ValueListObserver<Scope>\n  private scopesByIdentifierByElement: WeakMap<Element, Map<string, Scope>>\n  private scopeReferenceCounts: WeakMap<Scope, number>\n\n  constructor(element: Element, schema: Schema, delegate: ScopeObserverDelegate) {\n    this.element = element\n    this.schema = schema\n    this.delegate = delegate\n    this.valueListObserver = new ValueListObserver(this.element, this.controllerAttribute, this)\n    this.scopesByIdentifierByElement = new WeakMap()\n    this.scopeReferenceCounts = new WeakMap()\n  }\n\n  start() {\n    this.valueListObserver.start()\n  }\n\n  stop() {\n    this.valueListObserver.stop()\n  }\n\n  get controllerAttribute() {\n    return this.schema.controllerAttribute\n  }\n\n  // Value observer delegate\n\n  parseValueForToken(token: Token): Scope | undefined {\n    const { element, content: identifier } = token\n    return this.parseValueForElementAndIdentifier(element, identifier)\n  }\n\n  parseValueForElementAndIdentifier(element: Element, identifier: string): Scope | undefined {\n    const scopesByIdentifier = this.fetchScopesByIdentifierForElement(element)\n\n    let scope = scopesByIdentifier.get(identifier)\n    if (!scope) {\n      scope = this.delegate.createScopeForElementAndIdentifier(element, identifier)\n      scopesByIdentifier.set(identifier, scope)\n    }\n\n    return scope\n  }\n\n  elementMatchedValue(element: Element, value: Scope) {\n    const referenceCount = (this.scopeReferenceCounts.get(value) || 0) + 1\n    this.scopeReferenceCounts.set(value, referenceCount)\n    if (referenceCount == 1) {\n      this.delegate.scopeConnected(value)\n    }\n  }\n\n  elementUnmatchedValue(element: Element, value: Scope) {\n    const referenceCount = this.scopeReferenceCounts.get(value)\n    if (referenceCount) {\n      this.scopeReferenceCounts.set(value, referenceCount - 1)\n      if (referenceCount == 1) {\n        this.delegate.scopeDisconnected(value)\n      }\n    }\n  }\n\n  private fetchScopesByIdentifierForElement(element: Element) {\n    let scopesByIdentifier = this.scopesByIdentifierByElement.get(element)\n    if (!scopesByIdentifier) {\n      scopesByIdentifier = new Map()\n      this.scopesByIdentifierByElement.set(element, scopesByIdentifier)\n    }\n    return scopesByIdentifier\n  }\n}\n"
  },
  {
    "path": "src/core/selectors.ts",
    "content": "export function attributeValueContainsToken(attributeName: string, token: string) {\n  return `[${attributeName}~=\"${token}\"]`\n}\n"
  },
  {
    "path": "src/core/string_helpers.ts",
    "content": "export function camelize(value: string) {\n  return value.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase())\n}\n\nexport function namespaceCamelize(value: string) {\n  return camelize(value.replace(/--/g, \"-\").replace(/__/g, \"_\"))\n}\n\nexport function capitalize(value: string) {\n  return value.charAt(0).toUpperCase() + value.slice(1)\n}\n\nexport function dasherize(value: string) {\n  return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)\n}\n\nexport function tokenize(value: string) {\n  return value.match(/[^\\s]+/g) || []\n}\n"
  },
  {
    "path": "src/core/target_observer.ts",
    "content": "import { Multimap } from \"../multimap\"\nimport { Token, TokenListObserver, TokenListObserverDelegate } from \"../mutation-observers\"\nimport { Context } from \"./context\"\n\nexport interface TargetObserverDelegate {\n  targetConnected(element: Element, name: string): void\n  targetDisconnected(element: Element, name: string): void\n}\n\nexport class TargetObserver implements TokenListObserverDelegate {\n  readonly context: Context\n  readonly delegate: TargetObserverDelegate\n  readonly targetsByName: Multimap<string, Element>\n  private tokenListObserver?: TokenListObserver\n\n  constructor(context: Context, delegate: TargetObserverDelegate) {\n    this.context = context\n    this.delegate = delegate\n    this.targetsByName = new Multimap()\n  }\n\n  start() {\n    if (!this.tokenListObserver) {\n      this.tokenListObserver = new TokenListObserver(this.element, this.attributeName, this)\n      this.tokenListObserver.start()\n    }\n  }\n\n  stop() {\n    if (this.tokenListObserver) {\n      this.disconnectAllTargets()\n      this.tokenListObserver.stop()\n      delete this.tokenListObserver\n    }\n  }\n\n  // Token list observer delegate\n\n  tokenMatched({ element, content: name }: Token) {\n    if (this.scope.containsElement(element)) {\n      this.connectTarget(element, name)\n    }\n  }\n\n  tokenUnmatched({ element, content: name }: Token) {\n    this.disconnectTarget(element, name)\n  }\n\n  // Target management\n\n  connectTarget(element: Element, name: string) {\n    if (!this.targetsByName.has(name, element)) {\n      this.targetsByName.add(name, element)\n      this.tokenListObserver?.pause(() => this.delegate.targetConnected(element, name))\n    }\n  }\n\n  disconnectTarget(element: Element, name: string) {\n    if (this.targetsByName.has(name, element)) {\n      this.targetsByName.delete(name, element)\n      this.tokenListObserver?.pause(() => this.delegate.targetDisconnected(element, name))\n    }\n  }\n\n  disconnectAllTargets() {\n    for (const name of this.targetsByName.keys) {\n      for (const element of this.targetsByName.getValuesForKey(name)) {\n        this.disconnectTarget(element, name)\n      }\n    }\n  }\n\n  // Private\n\n  private get attributeName() {\n    return `data-${this.context.identifier}-target`\n  }\n\n  private get element() {\n    return this.context.element\n  }\n\n  private get scope() {\n    return this.context.scope\n  }\n}\n"
  },
  {
    "path": "src/core/target_properties.ts",
    "content": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticArrayValues } from \"./inheritable_statics\"\nimport { capitalize } from \"./string_helpers\"\n\nexport function TargetPropertiesBlessing<T>(constructor: Constructor<T>) {\n  const targets = readInheritableStaticArrayValues(constructor, \"targets\")\n  return targets.reduce((properties, targetDefinition) => {\n    return Object.assign(properties, propertiesForTargetDefinition(targetDefinition))\n  }, {} as PropertyDescriptorMap)\n}\n\nfunction propertiesForTargetDefinition(name: string) {\n  return {\n    [`${name}Target`]: {\n      get(this: Controller) {\n        const target = this.targets.find(name)\n        if (target) {\n          return target\n        } else {\n          throw new Error(`Missing target element \"${name}\" for \"${this.identifier}\" controller`)\n        }\n      },\n    },\n\n    [`${name}Targets`]: {\n      get(this: Controller) {\n        return this.targets.findAll(name)\n      },\n    },\n\n    [`has${capitalize(name)}Target`]: {\n      get(this: Controller) {\n        return this.targets.has(name)\n      },\n    },\n  }\n}\n"
  },
  {
    "path": "src/core/target_set.ts",
    "content": "import { Scope } from \"./scope\"\nimport { attributeValueContainsToken } from \"./selectors\"\n\nexport class TargetSet {\n  readonly scope: Scope\n\n  constructor(scope: Scope) {\n    this.scope = scope\n  }\n\n  get element() {\n    return this.scope.element\n  }\n\n  get identifier() {\n    return this.scope.identifier\n  }\n\n  get schema() {\n    return this.scope.schema\n  }\n\n  has(targetName: string) {\n    return this.find(targetName) != null\n  }\n\n  find(...targetNames: string[]) {\n    return targetNames.reduce(\n      (target, targetName) => target || this.findTarget(targetName) || this.findLegacyTarget(targetName),\n      undefined as Element | undefined\n    )\n  }\n\n  findAll(...targetNames: string[]) {\n    return targetNames.reduce(\n      (targets, targetName) => [\n        ...targets,\n        ...this.findAllTargets(targetName),\n        ...this.findAllLegacyTargets(targetName),\n      ],\n      [] as Element[]\n    )\n  }\n\n  private findTarget(targetName: string) {\n    const selector = this.getSelectorForTargetName(targetName)\n    return this.scope.findElement(selector)\n  }\n\n  private findAllTargets(targetName: string) {\n    const selector = this.getSelectorForTargetName(targetName)\n    return this.scope.findAllElements(selector)\n  }\n\n  private getSelectorForTargetName(targetName: string) {\n    const attributeName = this.schema.targetAttributeForScope(this.identifier)\n    return attributeValueContainsToken(attributeName, targetName)\n  }\n\n  private findLegacyTarget(targetName: string) {\n    const selector = this.getLegacySelectorForTargetName(targetName)\n    return this.deprecate(this.scope.findElement(selector), targetName)\n  }\n\n  private findAllLegacyTargets(targetName: string) {\n    const selector = this.getLegacySelectorForTargetName(targetName)\n    return this.scope.findAllElements(selector).map((element) => this.deprecate(element, targetName))\n  }\n\n  private getLegacySelectorForTargetName(targetName: string) {\n    const targetDescriptor = `${this.identifier}.${targetName}`\n    return attributeValueContainsToken(this.schema.targetAttribute, targetDescriptor)\n  }\n\n  private deprecate<T>(element: T, targetName: string) {\n    if (element) {\n      const { identifier } = this\n      const attributeName = this.schema.targetAttribute\n      const revisedAttributeName = this.schema.targetAttributeForScope(identifier)\n      this.guide.warn(\n        element,\n        `target:${targetName}`,\n        `Please replace ${attributeName}=\"${identifier}.${targetName}\" with ${revisedAttributeName}=\"${targetName}\". ` +\n          `The ${attributeName} attribute is deprecated and will be removed in a future version of Stimulus.`\n      )\n    }\n    return element\n  }\n\n  private get guide() {\n    return this.scope.guide\n  }\n}\n"
  },
  {
    "path": "src/core/utils.ts",
    "content": "export function isSomething(object: any): boolean {\n  return object !== null && object !== undefined\n}\n\nexport function hasProperty(object: any, property: string): boolean {\n  return Object.prototype.hasOwnProperty.call(object, property)\n}\n"
  },
  {
    "path": "src/core/value_observer.ts",
    "content": "import { Context } from \"./context\"\nimport { StringMapObserver, StringMapObserverDelegate } from \"../mutation-observers\"\nimport { ValueDescriptor } from \"./value_properties\"\nimport { capitalize } from \"./string_helpers\"\n\nexport class ValueObserver implements StringMapObserverDelegate {\n  readonly context: Context\n  readonly receiver: any\n  private stringMapObserver: StringMapObserver\n  private valueDescriptorMap: { [attributeName: string]: ValueDescriptor }\n\n  constructor(context: Context, receiver: any) {\n    this.context = context\n    this.receiver = receiver\n    this.stringMapObserver = new StringMapObserver(this.element, this)\n    this.valueDescriptorMap = (this.controller as any).valueDescriptorMap\n  }\n\n  start() {\n    this.stringMapObserver.start()\n    this.invokeChangedCallbacksForDefaultValues()\n  }\n\n  stop() {\n    this.stringMapObserver.stop()\n  }\n\n  get element() {\n    return this.context.element\n  }\n\n  get controller() {\n    return this.context.controller\n  }\n\n  // String map observer delegate\n\n  getStringMapKeyForAttribute(attributeName: string) {\n    if (attributeName in this.valueDescriptorMap) {\n      return this.valueDescriptorMap[attributeName].name\n    }\n  }\n\n  stringMapKeyAdded(key: string, attributeName: string) {\n    const descriptor = this.valueDescriptorMap[attributeName]\n\n    if (!this.hasValue(key)) {\n      this.invokeChangedCallback(key, descriptor.writer(this.receiver[key]), descriptor.writer(descriptor.defaultValue))\n    }\n  }\n\n  stringMapValueChanged(value: string, name: string, oldValue: string) {\n    const descriptor = this.valueDescriptorNameMap[name]\n\n    if (value === null) return\n\n    if (oldValue === null) {\n      oldValue = descriptor.writer(descriptor.defaultValue)\n    }\n\n    this.invokeChangedCallback(name, value, oldValue)\n  }\n\n  stringMapKeyRemoved(key: string, attributeName: string, oldValue: string) {\n    const descriptor = this.valueDescriptorNameMap[key]\n\n    if (this.hasValue(key)) {\n      this.invokeChangedCallback(key, descriptor.writer(this.receiver[key]), oldValue)\n    } else {\n      this.invokeChangedCallback(key, descriptor.writer(descriptor.defaultValue), oldValue)\n    }\n  }\n\n  private invokeChangedCallbacksForDefaultValues() {\n    for (const { key, name, defaultValue, writer } of this.valueDescriptors) {\n      if (defaultValue != undefined && !this.controller.data.has(key)) {\n        this.invokeChangedCallback(name, writer(defaultValue), undefined)\n      }\n    }\n  }\n\n  private invokeChangedCallback(name: string, rawValue: string, rawOldValue: string | undefined) {\n    const changedMethodName = `${name}Changed`\n    const changedMethod = this.receiver[changedMethodName]\n\n    if (typeof changedMethod == \"function\") {\n      const descriptor = this.valueDescriptorNameMap[name]\n\n      try {\n        const value = descriptor.reader(rawValue)\n        let oldValue = rawOldValue\n\n        if (rawOldValue) {\n          oldValue = descriptor.reader(rawOldValue)\n        }\n\n        changedMethod.call(this.receiver, value, oldValue)\n      } catch (error) {\n        if (error instanceof TypeError) {\n          error.message = `Stimulus Value \"${this.context.identifier}.${descriptor.name}\" - ${error.message}`\n        }\n\n        throw error\n      }\n    }\n  }\n\n  private get valueDescriptors() {\n    const { valueDescriptorMap } = this\n    return Object.keys(valueDescriptorMap).map((key) => valueDescriptorMap[key])\n  }\n\n  private get valueDescriptorNameMap() {\n    const descriptors: { [type: string]: ValueDescriptor } = {}\n\n    Object.keys(this.valueDescriptorMap).forEach((key) => {\n      const descriptor = this.valueDescriptorMap[key]\n      descriptors[descriptor.name] = descriptor\n    })\n\n    return descriptors\n  }\n\n  private hasValue(attributeName: string) {\n    const descriptor = this.valueDescriptorNameMap[attributeName]\n    const hasMethodName = `has${capitalize(descriptor.name)}`\n\n    return this.receiver[hasMethodName]\n  }\n}\n"
  },
  {
    "path": "src/core/value_properties.ts",
    "content": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticObjectPairs } from \"./inheritable_statics\"\nimport { camelize, capitalize, dasherize } from \"./string_helpers\"\nimport { isSomething, hasProperty } from \"./utils\"\n\nexport function ValuePropertiesBlessing<T>(constructor: Constructor<T>) {\n  const valueDefinitionPairs = readInheritableStaticObjectPairs<T, ValueTypeDefinition>(constructor, \"values\")\n  const propertyDescriptorMap: PropertyDescriptorMap = {\n    valueDescriptorMap: {\n      get(this: Controller) {\n        return valueDefinitionPairs.reduce((result, valueDefinitionPair) => {\n          const valueDescriptor = parseValueDefinitionPair(valueDefinitionPair, this.identifier)\n          const attributeName = this.data.getAttributeNameForKey(valueDescriptor.key)\n          return Object.assign(result, { [attributeName]: valueDescriptor })\n        }, {} as ValueDescriptorMap)\n      },\n    },\n  }\n\n  return valueDefinitionPairs.reduce((properties, valueDefinitionPair) => {\n    return Object.assign(properties, propertiesForValueDefinitionPair(valueDefinitionPair))\n  }, propertyDescriptorMap)\n}\n\nexport function propertiesForValueDefinitionPair<T>(\n  valueDefinitionPair: ValueDefinitionPair,\n  controller?: string\n): PropertyDescriptorMap {\n  const definition = parseValueDefinitionPair(valueDefinitionPair, controller)\n  const { key, name, reader: read, writer: write } = definition\n\n  return {\n    [name]: {\n      get(this: Controller) {\n        const value = this.data.get(key)\n        if (value !== null) {\n          return read(value)\n        } else {\n          return definition.defaultValue\n        }\n      },\n\n      set(this: Controller, value: T | undefined) {\n        if (value === undefined) {\n          this.data.delete(key)\n        } else {\n          this.data.set(key, write(value))\n        }\n      },\n    },\n\n    [`has${capitalize(name)}`]: {\n      get(this: Controller): boolean {\n        return this.data.has(key) || definition.hasCustomDefaultValue\n      },\n    },\n  }\n}\n\nexport type ValueDescriptor = {\n  type: ValueType\n  key: string\n  name: string\n  defaultValue: ValueTypeDefault\n  hasCustomDefaultValue: boolean\n  reader: Reader\n  writer: Writer\n}\n\nexport type ValueDescriptorMap = { [attributeName: string]: ValueDescriptor }\n\nexport type ValueDefinitionMap = { [token: string]: ValueTypeDefinition }\n\nexport type ValueDefinitionPair = [string, ValueTypeDefinition]\n\nexport type ValueTypeConstant = typeof Array | typeof Boolean | typeof Number | typeof Object | typeof String\n\nexport type ValueTypeDefault = Array<any> | boolean | number | Object | string\n\nexport type ValueTypeObject = Partial<{ type: ValueTypeConstant; default: ValueTypeDefault }>\n\nexport type ValueTypeDefinition = ValueTypeConstant | ValueTypeDefault | ValueTypeObject\n\nexport type ValueType = \"array\" | \"boolean\" | \"number\" | \"object\" | \"string\"\n\nfunction parseValueDefinitionPair([token, typeDefinition]: ValueDefinitionPair, controller?: string): ValueDescriptor {\n  return valueDescriptorForTokenAndTypeDefinition({\n    controller,\n    token,\n    typeDefinition,\n  })\n}\n\nexport function parseValueTypeConstant(constant?: ValueTypeConstant) {\n  switch (constant) {\n    case Array:\n      return \"array\"\n    case Boolean:\n      return \"boolean\"\n    case Number:\n      return \"number\"\n    case Object:\n      return \"object\"\n    case String:\n      return \"string\"\n  }\n}\n\nexport function parseValueTypeDefault(defaultValue?: ValueTypeDefault) {\n  switch (typeof defaultValue) {\n    case \"boolean\":\n      return \"boolean\"\n    case \"number\":\n      return \"number\"\n    case \"string\":\n      return \"string\"\n  }\n\n  if (Array.isArray(defaultValue)) return \"array\"\n  if (Object.prototype.toString.call(defaultValue) === \"[object Object]\") return \"object\"\n}\n\ntype ValueTypeObjectPayload = {\n  controller?: string\n  token: string\n  typeObject: ValueTypeObject\n}\n\nexport function parseValueTypeObject(payload: ValueTypeObjectPayload) {\n  const { controller, token, typeObject } = payload\n\n  const hasType = isSomething(typeObject.type)\n  const hasDefault = isSomething(typeObject.default)\n\n  const fullObject = hasType && hasDefault\n  const onlyType = hasType && !hasDefault\n  const onlyDefault = !hasType && hasDefault\n\n  const typeFromObject = parseValueTypeConstant(typeObject.type)\n  const typeFromDefaultValue = parseValueTypeDefault(payload.typeObject.default)\n\n  if (onlyType) return typeFromObject\n  if (onlyDefault) return typeFromDefaultValue\n\n  if (typeFromObject !== typeFromDefaultValue) {\n    const propertyPath = controller ? `${controller}.${token}` : token\n\n    throw new Error(\n      `The specified default value for the Stimulus Value \"${propertyPath}\" must match the defined type \"${typeFromObject}\". The provided default value of \"${typeObject.default}\" is of type \"${typeFromDefaultValue}\".`\n    )\n  }\n\n  if (fullObject) return typeFromObject\n}\n\ntype ValueTypeDefinitionPayload = {\n  controller?: string\n  token: string\n  typeDefinition: ValueTypeDefinition\n}\n\nexport function parseValueTypeDefinition(payload: ValueTypeDefinitionPayload): ValueType {\n  const { controller, token, typeDefinition } = payload\n\n  const typeObject = { controller, token, typeObject: typeDefinition as ValueTypeObject }\n\n  const typeFromObject = parseValueTypeObject(typeObject as ValueTypeObjectPayload)\n  const typeFromDefaultValue = parseValueTypeDefault(typeDefinition as ValueTypeDefault)\n  const typeFromConstant = parseValueTypeConstant(typeDefinition as ValueTypeConstant)\n\n  const type = typeFromObject || typeFromDefaultValue || typeFromConstant\n\n  if (type) return type\n\n  const propertyPath = controller ? `${controller}.${typeDefinition}` : token\n\n  throw new Error(`Unknown value type \"${propertyPath}\" for \"${token}\" value`)\n}\n\nexport function defaultValueForDefinition(typeDefinition: ValueTypeDefinition): ValueTypeDefault {\n  const constant = parseValueTypeConstant(typeDefinition as ValueTypeConstant)\n  if (constant) return defaultValuesByType[constant]\n\n  const hasDefault = hasProperty(typeDefinition, \"default\")\n  const hasType = hasProperty(typeDefinition, \"type\")\n  const typeObject = typeDefinition as ValueTypeObject\n\n  if (hasDefault) return typeObject.default!\n\n  if (hasType) {\n    const { type } = typeObject\n    const constantFromType = parseValueTypeConstant(type)\n\n    if (constantFromType) return defaultValuesByType[constantFromType]\n  }\n\n  return typeDefinition\n}\n\nfunction valueDescriptorForTokenAndTypeDefinition(payload: ValueTypeDefinitionPayload) {\n  const { token, typeDefinition } = payload\n\n  const key = `${dasherize(token)}-value`\n  const type = parseValueTypeDefinition(payload)\n  return {\n    type,\n    key,\n    name: camelize(key),\n    get defaultValue() {\n      return defaultValueForDefinition(typeDefinition)\n    },\n    get hasCustomDefaultValue() {\n      return parseValueTypeDefault(typeDefinition) !== undefined\n    },\n    reader: readers[type],\n    writer: writers[type] || writers.default,\n  }\n}\n\nconst defaultValuesByType = {\n  get array() {\n    return []\n  },\n  boolean: false,\n  number: 0,\n  get object() {\n    return {}\n  },\n  string: \"\",\n}\n\ntype Reader = (value: string) => any\n\nconst readers: { [type: string]: Reader } = {\n  array(value: string): any[] {\n    const array = JSON.parse(value)\n    if (!Array.isArray(array)) {\n      throw new TypeError(\n        `expected value of type \"array\" but instead got value \"${value}\" of type \"${parseValueTypeDefault(array)}\"`\n      )\n    }\n    return array\n  },\n\n  boolean(value: string): boolean {\n    return !(value == \"0\" || String(value).toLowerCase() == \"false\")\n  },\n\n  number(value: string): number {\n    return Number(value.replace(/_/g, \"\"))\n  },\n\n  object(value: string): object {\n    const object = JSON.parse(value)\n    if (object === null || typeof object != \"object\" || Array.isArray(object)) {\n      throw new TypeError(\n        `expected value of type \"object\" but instead got value \"${value}\" of type \"${parseValueTypeDefault(object)}\"`\n      )\n    }\n    return object\n  },\n\n  string(value: string): string {\n    return value\n  },\n}\n\ntype Writer = (value: any) => string\n\nconst writers: { [type: string]: Writer } = {\n  default: writeString,\n  array: writeJSON,\n  object: writeJSON,\n}\n\nfunction writeJSON(value: any) {\n  return JSON.stringify(value)\n}\n\nfunction writeString(value: any) {\n  return `${value}`\n}\n"
  },
  {
    "path": "src/index.d.ts",
    "content": "export * from \"./core\"\nexport as namespace Stimulus\n"
  },
  {
    "path": "src/index.js",
    "content": "export * from \"./core\"\nexport * from \"./multimap\"\nexport * from \"./mutation-observers\"\n"
  },
  {
    "path": "src/index.ts",
    "content": "export * from \"./core\"\nexport * from \"./multimap\"\nexport * from \"./mutation-observers\"\n"
  },
  {
    "path": "src/multimap/index.ts",
    "content": "export * from \"./indexed_multimap\"\nexport * from \"./multimap\"\nexport * from \"./set_operations\"\n"
  },
  {
    "path": "src/multimap/indexed_multimap.ts",
    "content": "import { Multimap } from \"./multimap\"\nimport { add, del } from \"./set_operations\"\n\nexport class IndexedMultimap<K, V> extends Multimap<K, V> {\n  private keysByValue: Map<V, Set<K>>\n\n  constructor() {\n    super()\n    this.keysByValue = new Map()\n  }\n\n  get values(): V[] {\n    return Array.from(this.keysByValue.keys())\n  }\n\n  add(key: K, value: V) {\n    super.add(key, value)\n    add(this.keysByValue, value, key)\n  }\n\n  delete(key: K, value: V) {\n    super.delete(key, value)\n    del(this.keysByValue, value, key)\n  }\n\n  hasValue(value: V): boolean {\n    return this.keysByValue.has(value)\n  }\n\n  getKeysForValue(value: V): K[] {\n    const set = this.keysByValue.get(value)\n    return set ? Array.from(set) : []\n  }\n}\n"
  },
  {
    "path": "src/multimap/multimap.ts",
    "content": "import { add, del } from \"./set_operations\"\n\nexport class Multimap<K, V> {\n  private valuesByKey: Map<K, Set<V>>\n\n  constructor() {\n    this.valuesByKey = new Map<K, Set<V>>()\n  }\n\n  get keys() {\n    return Array.from(this.valuesByKey.keys())\n  }\n\n  get values(): V[] {\n    const sets = Array.from(this.valuesByKey.values())\n    return sets.reduce((values, set) => values.concat(Array.from(set)), <V[]>[])\n  }\n\n  get size(): number {\n    const sets = Array.from(this.valuesByKey.values())\n    return sets.reduce((size, set) => size + set.size, 0)\n  }\n\n  add(key: K, value: V) {\n    add(this.valuesByKey, key, value)\n  }\n\n  delete(key: K, value: V) {\n    del(this.valuesByKey, key, value)\n  }\n\n  has(key: K, value: V): boolean {\n    const values = this.valuesByKey.get(key)\n    return values != null && values.has(value)\n  }\n\n  hasKey(key: K): boolean {\n    return this.valuesByKey.has(key)\n  }\n\n  hasValue(value: V): boolean {\n    const sets = Array.from(this.valuesByKey.values())\n    return sets.some((set) => set.has(value))\n  }\n\n  getValuesForKey(key: K): V[] {\n    const values = this.valuesByKey.get(key)\n    return values ? Array.from(values) : []\n  }\n\n  getKeysForValue(value: V): K[] {\n    return Array.from(this.valuesByKey)\n      .filter(([_key, values]) => values.has(value))\n      .map(([key, _values]) => key)\n  }\n}\n"
  },
  {
    "path": "src/multimap/set_operations.ts",
    "content": "export function add<K, V>(map: Map<K, Set<V>>, key: K, value: V) {\n  fetch(map, key).add(value)\n}\n\nexport function del<K, V>(map: Map<K, Set<V>>, key: K, value: V) {\n  fetch(map, key).delete(value)\n  prune(map, key)\n}\n\nexport function fetch<K, V>(map: Map<K, Set<V>>, key: K): Set<V> {\n  let values = map.get(key)\n  if (!values) {\n    values = new Set()\n    map.set(key, values)\n  }\n  return values\n}\n\nexport function prune<K, V>(map: Map<K, Set<V>>, key: K) {\n  const values = map.get(key)\n  if (values != null && values.size == 0) {\n    map.delete(key)\n  }\n}\n"
  },
  {
    "path": "src/mutation-observers/attribute_observer.ts",
    "content": "import { ElementObserver, ElementObserverDelegate } from \"./element_observer\"\n\nexport interface AttributeObserverDelegate {\n  elementMatchedAttribute?(element: Element, attributeName: string): void\n  elementAttributeValueChanged?(element: Element, attributeName: string): void\n  elementUnmatchedAttribute?(element: Element, attributeName: string): void\n}\n\nexport class AttributeObserver implements ElementObserverDelegate {\n  attributeName: string\n  private delegate: AttributeObserverDelegate\n\n  private elementObserver: ElementObserver\n\n  constructor(element: Element, attributeName: string, delegate: AttributeObserverDelegate) {\n    this.attributeName = attributeName\n    this.delegate = delegate\n\n    this.elementObserver = new ElementObserver(element, this)\n  }\n\n  get element(): Element {\n    return this.elementObserver.element\n  }\n\n  get selector(): string {\n    return `[${this.attributeName}]`\n  }\n\n  start() {\n    this.elementObserver.start()\n  }\n\n  pause(callback: () => void) {\n    this.elementObserver.pause(callback)\n  }\n\n  stop() {\n    this.elementObserver.stop()\n  }\n\n  refresh() {\n    this.elementObserver.refresh()\n  }\n\n  get started(): boolean {\n    return this.elementObserver.started\n  }\n\n  // Element observer delegate\n\n  matchElement(element: Element): boolean {\n    return element.hasAttribute(this.attributeName)\n  }\n\n  matchElementsInTree(tree: Element): Element[] {\n    const match = this.matchElement(tree) ? [tree] : []\n    const matches = Array.from(tree.querySelectorAll(this.selector))\n    return match.concat(matches)\n  }\n\n  elementMatched(element: Element) {\n    if (this.delegate.elementMatchedAttribute) {\n      this.delegate.elementMatchedAttribute(element, this.attributeName)\n    }\n  }\n\n  elementUnmatched(element: Element) {\n    if (this.delegate.elementUnmatchedAttribute) {\n      this.delegate.elementUnmatchedAttribute(element, this.attributeName)\n    }\n  }\n\n  elementAttributeChanged(element: Element, attributeName: string) {\n    if (this.delegate.elementAttributeValueChanged && this.attributeName == attributeName) {\n      this.delegate.elementAttributeValueChanged(element, attributeName)\n    }\n  }\n}\n"
  },
  {
    "path": "src/mutation-observers/element_observer.ts",
    "content": "export interface ElementObserverDelegate {\n  matchElement(element: Element): boolean\n  matchElementsInTree(tree: Element): Element[]\n\n  elementMatched?(element: Element): void\n  elementUnmatched?(element: Element): void\n  elementAttributeChanged?(element: Element, attributeName: string): void\n}\n\nexport class ElementObserver {\n  element: Element\n  started: boolean\n  private delegate: ElementObserverDelegate\n\n  private elements: Set<Element>\n  private mutationObserver: MutationObserver\n  private mutationObserverInit: MutationObserverInit = { attributes: true, childList: true, subtree: true }\n\n  constructor(element: Element, delegate: ElementObserverDelegate) {\n    this.element = element\n    this.started = false\n    this.delegate = delegate\n\n    this.elements = new Set()\n    this.mutationObserver = new MutationObserver((mutations) => this.processMutations(mutations))\n  }\n\n  start() {\n    if (!this.started) {\n      this.started = true\n      this.mutationObserver.observe(this.element, this.mutationObserverInit)\n      this.refresh()\n    }\n  }\n\n  pause(callback: () => void) {\n    if (this.started) {\n      this.mutationObserver.disconnect()\n      this.started = false\n    }\n\n    callback()\n\n    if (!this.started) {\n      this.mutationObserver.observe(this.element, this.mutationObserverInit)\n      this.started = true\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      this.mutationObserver.takeRecords()\n      this.mutationObserver.disconnect()\n      this.started = false\n    }\n  }\n\n  refresh() {\n    if (this.started) {\n      const matches = new Set(this.matchElementsInTree())\n\n      for (const element of Array.from(this.elements)) {\n        if (!matches.has(element)) {\n          this.removeElement(element)\n        }\n      }\n\n      for (const element of Array.from(matches)) {\n        this.addElement(element)\n      }\n    }\n  }\n\n  // Mutation record processing\n\n  private processMutations(mutations: MutationRecord[]) {\n    if (this.started) {\n      for (const mutation of mutations) {\n        this.processMutation(mutation)\n      }\n    }\n  }\n\n  private processMutation(mutation: MutationRecord) {\n    if (mutation.type == \"attributes\") {\n      this.processAttributeChange(mutation.target as Element, mutation.attributeName!)\n    } else if (mutation.type == \"childList\") {\n      this.processRemovedNodes(mutation.removedNodes)\n      this.processAddedNodes(mutation.addedNodes)\n    }\n  }\n\n  private processAttributeChange(element: Element, attributeName: string) {\n    if (this.elements.has(element)) {\n      if (this.delegate.elementAttributeChanged && this.matchElement(element)) {\n        this.delegate.elementAttributeChanged(element, attributeName)\n      } else {\n        this.removeElement(element)\n      }\n    } else if (this.matchElement(element)) {\n      this.addElement(element)\n    }\n  }\n\n  private processRemovedNodes(nodes: NodeList) {\n    for (const node of Array.from(nodes)) {\n      const element = this.elementFromNode(node)\n      if (element) {\n        this.processTree(element, this.removeElement)\n      }\n    }\n  }\n\n  private processAddedNodes(nodes: NodeList) {\n    for (const node of Array.from(nodes)) {\n      const element = this.elementFromNode(node)\n      if (element && this.elementIsActive(element)) {\n        this.processTree(element, this.addElement)\n      }\n    }\n  }\n\n  // Element matching\n\n  private matchElement(element: Element): boolean {\n    return this.delegate.matchElement(element)\n  }\n\n  private matchElementsInTree(tree: Element = this.element): Element[] {\n    return this.delegate.matchElementsInTree(tree)\n  }\n\n  private processTree(tree: Element, processor: (element: Element) => void) {\n    for (const element of this.matchElementsInTree(tree)) {\n      processor.call(this, element)\n    }\n  }\n\n  private elementFromNode(node: Node): Element | undefined {\n    if (node.nodeType == Node.ELEMENT_NODE) {\n      return node as Element\n    }\n  }\n\n  private elementIsActive(element: Element): boolean {\n    if (element.isConnected != this.element.isConnected) {\n      return false\n    } else {\n      return this.element.contains(element)\n    }\n  }\n\n  // Element tracking\n\n  private addElement(element: Element) {\n    if (!this.elements.has(element)) {\n      if (this.elementIsActive(element)) {\n        this.elements.add(element)\n        if (this.delegate.elementMatched) {\n          this.delegate.elementMatched(element)\n        }\n      }\n    }\n  }\n\n  private removeElement(element: Element) {\n    if (this.elements.has(element)) {\n      this.elements.delete(element)\n      if (this.delegate.elementUnmatched) {\n        this.delegate.elementUnmatched(element)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/mutation-observers/index.ts",
    "content": "export * from \"./attribute_observer\"\nexport * from \"./element_observer\"\nexport * from \"./selector_observer\"\nexport * from \"./string_map_observer\"\nexport * from \"./token_list_observer\"\nexport * from \"./value_list_observer\"\n"
  },
  {
    "path": "src/mutation-observers/selector_observer.ts",
    "content": "import { ElementObserver, ElementObserverDelegate } from \"./element_observer\"\nimport { Multimap } from \"../multimap\"\n\nexport interface SelectorObserverDelegate {\n  selectorMatched(element: Element, selector: string, details: object): void\n  selectorUnmatched(element: Element, selector: string, details: object): void\n  selectorMatchElement?(element: Element, details: object): boolean\n}\n\nexport class SelectorObserver implements ElementObserverDelegate {\n  private readonly elementObserver: ElementObserver\n  private readonly delegate: SelectorObserverDelegate\n  private readonly matchesByElement: Multimap<string, Element>\n  private readonly details: object\n  _selector: string | null\n\n  constructor(element: Element, selector: string, delegate: SelectorObserverDelegate, details: object) {\n    this._selector = selector\n    this.details = details\n    this.elementObserver = new ElementObserver(element, this)\n    this.delegate = delegate\n    this.matchesByElement = new Multimap()\n  }\n\n  get started(): boolean {\n    return this.elementObserver.started\n  }\n\n  get selector() {\n    return this._selector\n  }\n\n  set selector(selector: string | null) {\n    this._selector = selector\n    this.refresh()\n  }\n\n  start() {\n    this.elementObserver.start()\n  }\n\n  pause(callback: () => void) {\n    this.elementObserver.pause(callback)\n  }\n\n  stop() {\n    this.elementObserver.stop()\n  }\n\n  refresh() {\n    this.elementObserver.refresh()\n  }\n\n  get element(): Element {\n    return this.elementObserver.element\n  }\n\n  // Element observer delegate\n\n  matchElement(element: Element): boolean {\n    const { selector } = this\n\n    if (selector) {\n      const matches = element.matches(selector)\n\n      if (this.delegate.selectorMatchElement) {\n        return matches && this.delegate.selectorMatchElement(element, this.details)\n      }\n\n      return matches\n    } else {\n      return false\n    }\n  }\n\n  matchElementsInTree(tree: Element): Element[] {\n    const { selector } = this\n\n    if (selector) {\n      const match = this.matchElement(tree) ? [tree] : []\n      const matches = Array.from(tree.querySelectorAll(selector)).filter((match) => this.matchElement(match))\n      return match.concat(matches)\n    } else {\n      return []\n    }\n  }\n\n  elementMatched(element: Element) {\n    const { selector } = this\n\n    if (selector) {\n      this.selectorMatched(element, selector)\n    }\n  }\n\n  elementUnmatched(element: Element) {\n    const selectors = this.matchesByElement.getKeysForValue(element)\n\n    for (const selector of selectors) {\n      this.selectorUnmatched(element, selector)\n    }\n  }\n\n  elementAttributeChanged(element: Element, _attributeName: string) {\n    const { selector } = this\n\n    if (selector) {\n      const matches = this.matchElement(element)\n      const matchedBefore = this.matchesByElement.has(selector, element)\n\n      if (matches && !matchedBefore) {\n        this.selectorMatched(element, selector)\n      } else if (!matches && matchedBefore) {\n        this.selectorUnmatched(element, selector)\n      }\n    }\n  }\n\n  // Selector management\n\n  private selectorMatched(element: Element, selector: string) {\n    this.delegate.selectorMatched(element, selector, this.details)\n    this.matchesByElement.add(selector, element)\n  }\n\n  private selectorUnmatched(element: Element, selector: string) {\n    this.delegate.selectorUnmatched(element, selector, this.details)\n    this.matchesByElement.delete(selector, element)\n  }\n}\n"
  },
  {
    "path": "src/mutation-observers/string_map_observer.ts",
    "content": "export interface StringMapObserverDelegate {\n  getStringMapKeyForAttribute(attributeName: string): string | undefined\n  stringMapKeyAdded?(key: string, attributeName: string): void\n  stringMapValueChanged?(value: string | null, key: string, oldValue: string | null): void\n  stringMapKeyRemoved?(key: string, attributeName: string, oldValue: string | null): void\n}\n\nexport class StringMapObserver {\n  readonly element: Element\n  readonly delegate: StringMapObserverDelegate\n  private started: boolean\n  private stringMap: Map<string, string>\n  private mutationObserver: MutationObserver\n\n  constructor(element: Element, delegate: StringMapObserverDelegate) {\n    this.element = element\n    this.delegate = delegate\n    this.started = false\n    this.stringMap = new Map()\n    this.mutationObserver = new MutationObserver((mutations) => this.processMutations(mutations))\n  }\n\n  start() {\n    if (!this.started) {\n      this.started = true\n      this.mutationObserver.observe(this.element, { attributes: true, attributeOldValue: true })\n      this.refresh()\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      this.mutationObserver.takeRecords()\n      this.mutationObserver.disconnect()\n      this.started = false\n    }\n  }\n\n  refresh() {\n    if (this.started) {\n      for (const attributeName of this.knownAttributeNames) {\n        this.refreshAttribute(attributeName, null)\n      }\n    }\n  }\n\n  // Mutation record processing\n\n  private processMutations(mutations: MutationRecord[]) {\n    if (this.started) {\n      for (const mutation of mutations) {\n        this.processMutation(mutation)\n      }\n    }\n  }\n\n  private processMutation(mutation: MutationRecord) {\n    const attributeName = mutation.attributeName\n    if (attributeName) {\n      this.refreshAttribute(attributeName, mutation.oldValue)\n    }\n  }\n\n  // State tracking\n\n  private refreshAttribute(attributeName: string, oldValue: string | null) {\n    const key = this.delegate.getStringMapKeyForAttribute(attributeName)\n    if (key != null) {\n      if (!this.stringMap.has(attributeName)) {\n        this.stringMapKeyAdded(key, attributeName)\n      }\n\n      const value = this.element.getAttribute(attributeName)\n      if (this.stringMap.get(attributeName) != value) {\n        this.stringMapValueChanged(value, key, oldValue)\n      }\n\n      if (value == null) {\n        const oldValue = this.stringMap.get(attributeName)\n        this.stringMap.delete(attributeName)\n        if (oldValue) this.stringMapKeyRemoved(key, attributeName, oldValue)\n      } else {\n        this.stringMap.set(attributeName, value)\n      }\n    }\n  }\n\n  private stringMapKeyAdded(key: string, attributeName: string) {\n    if (this.delegate.stringMapKeyAdded) {\n      this.delegate.stringMapKeyAdded(key, attributeName)\n    }\n  }\n\n  private stringMapValueChanged(value: string | null, key: string, oldValue: string | null) {\n    if (this.delegate.stringMapValueChanged) {\n      this.delegate.stringMapValueChanged(value, key, oldValue)\n    }\n  }\n\n  private stringMapKeyRemoved(key: string, attributeName: string, oldValue: string | null) {\n    if (this.delegate.stringMapKeyRemoved) {\n      this.delegate.stringMapKeyRemoved(key, attributeName, oldValue)\n    }\n  }\n\n  private get knownAttributeNames() {\n    return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))\n  }\n\n  private get currentAttributeNames() {\n    return Array.from(this.element.attributes).map((attribute) => attribute.name)\n  }\n\n  private get recordedAttributeNames() {\n    return Array.from(this.stringMap.keys())\n  }\n}\n"
  },
  {
    "path": "src/mutation-observers/token_list_observer.ts",
    "content": "import { AttributeObserver, AttributeObserverDelegate } from \"./attribute_observer\"\nimport { Multimap } from \"../multimap\"\n\nexport interface Token {\n  element: Element\n  attributeName: string\n  index: number\n  content: string\n}\n\nexport interface TokenListObserverDelegate {\n  tokenMatched(token: Token): void\n  tokenUnmatched(token: Token): void\n}\n\nexport class TokenListObserver implements AttributeObserverDelegate {\n  private attributeObserver: AttributeObserver\n  private delegate: TokenListObserverDelegate\n  private tokensByElement: Multimap<Element, Token>\n\n  constructor(element: Element, attributeName: string, delegate: TokenListObserverDelegate) {\n    this.attributeObserver = new AttributeObserver(element, attributeName, this)\n    this.delegate = delegate\n    this.tokensByElement = new Multimap()\n  }\n\n  get started(): boolean {\n    return this.attributeObserver.started\n  }\n\n  start() {\n    this.attributeObserver.start()\n  }\n\n  pause(callback: () => void) {\n    this.attributeObserver.pause(callback)\n  }\n\n  stop() {\n    this.attributeObserver.stop()\n  }\n\n  refresh() {\n    this.attributeObserver.refresh()\n  }\n\n  get element(): Element {\n    return this.attributeObserver.element\n  }\n\n  get attributeName(): string {\n    return this.attributeObserver.attributeName\n  }\n\n  // Attribute observer delegate\n\n  elementMatchedAttribute(element: Element) {\n    this.tokensMatched(this.readTokensForElement(element))\n  }\n\n  elementAttributeValueChanged(element: Element) {\n    const [unmatchedTokens, matchedTokens] = this.refreshTokensForElement(element)\n    this.tokensUnmatched(unmatchedTokens)\n    this.tokensMatched(matchedTokens)\n  }\n\n  elementUnmatchedAttribute(element: Element) {\n    this.tokensUnmatched(this.tokensByElement.getValuesForKey(element))\n  }\n\n  private tokensMatched(tokens: Token[]) {\n    tokens.forEach((token) => this.tokenMatched(token))\n  }\n\n  private tokensUnmatched(tokens: Token[]) {\n    tokens.forEach((token) => this.tokenUnmatched(token))\n  }\n\n  private tokenMatched(token: Token) {\n    this.delegate.tokenMatched(token)\n    this.tokensByElement.add(token.element, token)\n  }\n\n  private tokenUnmatched(token: Token) {\n    this.delegate.tokenUnmatched(token)\n    this.tokensByElement.delete(token.element, token)\n  }\n\n  private refreshTokensForElement(element: Element): [Token[], Token[]] {\n    const previousTokens = this.tokensByElement.getValuesForKey(element)\n    const currentTokens = this.readTokensForElement(element)\n    const firstDifferingIndex = zip(previousTokens, currentTokens).findIndex(\n      ([previousToken, currentToken]) => !tokensAreEqual(previousToken, currentToken)\n    )\n\n    if (firstDifferingIndex == -1) {\n      return [[], []]\n    } else {\n      return [previousTokens.slice(firstDifferingIndex), currentTokens.slice(firstDifferingIndex)]\n    }\n  }\n\n  private readTokensForElement(element: Element): Token[] {\n    const attributeName = this.attributeName\n    const tokenString = element.getAttribute(attributeName) || \"\"\n    return parseTokenString(tokenString, element, attributeName)\n  }\n}\n\nfunction parseTokenString(tokenString: string, element: Element, attributeName: string): Token[] {\n  return tokenString\n    .trim()\n    .split(/\\s+/)\n    .filter((content) => content.length)\n    .map((content, index) => ({ element, attributeName, content, index }))\n}\n\nfunction zip<L, R>(left: L[], right: R[]): [L | undefined, R | undefined][] {\n  const length = Math.max(left.length, right.length)\n  return Array.from({ length }, (_, index) => [left[index], right[index]] as [L, R])\n}\n\nfunction tokensAreEqual(left?: Token, right?: Token) {\n  return left && right && left.index == right.index && left.content == right.content\n}\n"
  },
  {
    "path": "src/mutation-observers/value_list_observer.ts",
    "content": "import { Token, TokenListObserver, TokenListObserverDelegate } from \"./token_list_observer\"\n\nexport interface ValueListObserverDelegate<T> {\n  parseValueForToken(token: Token): T | undefined\n  elementMatchedValue(element: Element, value: T): void\n  elementUnmatchedValue(element: Element, value: T): void\n}\n\ninterface ParseResult<T> {\n  value?: T\n  error?: Error\n}\n\nexport class ValueListObserver<T> implements TokenListObserverDelegate {\n  private tokenListObserver: TokenListObserver\n  private delegate: ValueListObserverDelegate<T>\n  private parseResultsByToken: WeakMap<Token, ParseResult<T>>\n  private valuesByTokenByElement: WeakMap<Element, Map<Token, T>>\n\n  constructor(element: Element, attributeName: string, delegate: ValueListObserverDelegate<T>) {\n    this.tokenListObserver = new TokenListObserver(element, attributeName, this)\n    this.delegate = delegate\n    this.parseResultsByToken = new WeakMap()\n    this.valuesByTokenByElement = new WeakMap()\n  }\n\n  get started(): boolean {\n    return this.tokenListObserver.started\n  }\n\n  start() {\n    this.tokenListObserver.start()\n  }\n\n  stop() {\n    this.tokenListObserver.stop()\n  }\n\n  refresh() {\n    this.tokenListObserver.refresh()\n  }\n\n  get element(): Element {\n    return this.tokenListObserver.element\n  }\n\n  get attributeName(): string {\n    return this.tokenListObserver.attributeName\n  }\n\n  tokenMatched(token: Token) {\n    const { element } = token\n    const { value } = this.fetchParseResultForToken(token)\n    if (value) {\n      this.fetchValuesByTokenForElement(element).set(token, value)\n      this.delegate.elementMatchedValue(element, value)\n    }\n  }\n\n  tokenUnmatched(token: Token) {\n    const { element } = token\n    const { value } = this.fetchParseResultForToken(token)\n    if (value) {\n      this.fetchValuesByTokenForElement(element).delete(token)\n      this.delegate.elementUnmatchedValue(element, value)\n    }\n  }\n\n  private fetchParseResultForToken(token: Token) {\n    let parseResult = this.parseResultsByToken.get(token)\n    if (!parseResult) {\n      parseResult = this.parseToken(token)\n      this.parseResultsByToken.set(token, parseResult)\n    }\n    return parseResult\n  }\n\n  private fetchValuesByTokenForElement(element: Element) {\n    let valuesByToken = this.valuesByTokenByElement.get(element)\n    if (!valuesByToken) {\n      valuesByToken = new Map()\n      this.valuesByTokenByElement.set(element, valuesByToken)\n    }\n    return valuesByToken\n  }\n\n  private parseToken(token: Token): ParseResult<T> {\n    try {\n      const value = this.delegate.parseValueForToken(token)\n      return { value }\n    } catch (error: any) {\n      return { error }\n    }\n  }\n}\n"
  },
  {
    "path": "src/tests/cases/application_test_case.ts",
    "content": "import { Application } from \"../../core/application\"\nimport { DOMTestCase } from \"./dom_test_case\"\nimport { Schema, defaultSchema } from \"../../core/schema\"\n\nexport class TestApplication extends Application {\n  handleError(error: Error, _message: string, _detail: object) {\n    throw error\n  }\n}\n\nexport class ApplicationTestCase extends DOMTestCase {\n  schema: Schema = defaultSchema\n  application!: Application\n\n  async runTest(testName: string) {\n    try {\n      this.application = new TestApplication(this.fixtureElement, this.schema)\n      this.setupApplication()\n      this.application.start()\n      await super.runTest(testName)\n    } finally {\n      this.application.stop()\n    }\n  }\n\n  setupApplication() {\n    // Override in subclasses to register controllers\n  }\n}\n"
  },
  {
    "path": "src/tests/cases/controller_test_case.ts",
    "content": "import { ApplicationTestCase } from \"./application_test_case\"\nimport { Constructor } from \"../../core/constructor\"\nimport { Controller, ControllerConstructor } from \"../../core/controller\"\n\nexport class ControllerTests<T extends Controller> extends ApplicationTestCase {\n  identifier: string | string[] = \"test\"\n  controllerConstructor!: ControllerConstructor\n  fixtureHTML = `<div data-controller=\"${this.identifiers.join(\" \")}\">`\n\n  setupApplication() {\n    this.identifiers.forEach((identifier) => {\n      this.application.register(identifier, this.controllerConstructor)\n    })\n  }\n\n  get controller(): T {\n    const controller = this.controllers[0]\n    if (controller) {\n      return controller\n    } else {\n      throw new Error(\"no controller connected\")\n    }\n  }\n\n  get identifiers(): string[] {\n    if (typeof this.identifier == \"string\") {\n      return [this.identifier]\n    } else {\n      return this.identifier\n    }\n  }\n\n  get controllers(): T[] {\n    return this.application.controllers as any as T[]\n  }\n}\n\nexport function ControllerTestCase(): Constructor<ControllerTests<Controller>>\nexport function ControllerTestCase<T extends Controller>(constructor: Constructor<T>): Constructor<ControllerTests<T>>\nexport function ControllerTestCase<T extends Controller>(\n  constructor?: Constructor<T>\n): Constructor<ControllerTests<T>> {\n  return class extends ControllerTests<T> {\n    controllerConstructor = constructor || (Controller as any)\n  } as any\n}\n"
  },
  {
    "path": "src/tests/cases/dom_test_case.ts",
    "content": "import { TestCase } from \"./test_case\"\n\ninterface TriggerEventOptions {\n  bubbles?: boolean\n  setDefaultPrevented?: boolean\n}\n\nconst defaultTriggerEventOptions: TriggerEventOptions = {\n  bubbles: true,\n  setDefaultPrevented: true,\n}\n\nexport class DOMTestCase extends TestCase {\n  fixtureSelector = \"#qunit-fixture\"\n  fixtureHTML = \"\"\n\n  async runTest(testName: string) {\n    await this.renderFixture()\n    await super.runTest(testName)\n  }\n\n  async renderFixture(fixtureHTML = this.fixtureHTML) {\n    this.fixtureElement.innerHTML = fixtureHTML\n    return this.nextFrame\n  }\n\n  get fixtureElement(): Element {\n    const element = document.querySelector(this.fixtureSelector)\n    if (element) {\n      return element\n    } else {\n      throw new Error(`missing fixture element \"${this.fixtureSelector}\"`)\n    }\n  }\n\n  async triggerEvent(selectorOrTarget: string | EventTarget, type: string, options: TriggerEventOptions = {}) {\n    const { bubbles, setDefaultPrevented } = { ...defaultTriggerEventOptions, ...options }\n    const eventTarget = typeof selectorOrTarget == \"string\" ? this.findElement(selectorOrTarget) : selectorOrTarget\n    const event = document.createEvent(\"Events\")\n    event.initEvent(type, bubbles, true)\n\n    // IE <= 11 does not set `defaultPrevented` when `preventDefault()` is called on synthetic events\n    if (setDefaultPrevented) {\n      event.preventDefault = function () {\n        Object.defineProperty(this, \"defaultPrevented\", { get: () => true, configurable: true })\n      }\n    }\n\n    eventTarget.dispatchEvent(event)\n    await this.nextFrame\n    return event\n  }\n\n  async triggerMouseEvent(selectorOrTarget: string | EventTarget, type: string, options: MouseEventInit = {}) {\n    const eventTarget = typeof selectorOrTarget == \"string\" ? this.findElement(selectorOrTarget) : selectorOrTarget\n    const event = new MouseEvent(type, options)\n\n    eventTarget.dispatchEvent(event)\n    await this.nextFrame\n    return event\n  }\n\n  async triggerKeyboardEvent(selectorOrTarget: string | EventTarget, type: string, options: KeyboardEventInit = {}) {\n    const eventTarget = typeof selectorOrTarget == \"string\" ? this.findElement(selectorOrTarget) : selectorOrTarget\n    const event = new KeyboardEvent(type, options)\n\n    eventTarget.dispatchEvent(event)\n    await this.nextFrame\n    return event\n  }\n\n  async setAttribute(selectorOrElement: string | Element, name: string, value: string) {\n    const element = typeof selectorOrElement == \"string\" ? this.findElement(selectorOrElement) : selectorOrElement\n\n    element.setAttribute(name, value)\n    await this.nextFrame\n  }\n\n  async removeAttribute(selectorOrElement: string | Element, name: string) {\n    const element = typeof selectorOrElement == \"string\" ? this.findElement(selectorOrElement) : selectorOrElement\n\n    element.removeAttribute(name)\n    await this.nextFrame\n  }\n\n  async appendChild<T extends Node>(selectorOrElement: T | string, child: T) {\n    const parent = typeof selectorOrElement == \"string\" ? this.findElement(selectorOrElement) : selectorOrElement\n\n    parent.appendChild(child)\n    await this.nextFrame\n  }\n\n  async remove(selectorOrElement: Element | string) {\n    const element = typeof selectorOrElement == \"string\" ? this.findElement(selectorOrElement) : selectorOrElement\n\n    element.remove()\n    await this.nextFrame\n  }\n\n  findElement(selector: string) {\n    const element = this.fixtureElement.querySelector(selector)\n    if (element) {\n      return element\n    } else {\n      throw new Error(`couldn't find element \"${selector}\"`)\n    }\n  }\n\n  findElements(...selectors: string[]) {\n    return selectors.map((selector) => this.findElement(selector))\n  }\n\n  get nextFrame(): Promise<any> {\n    return new Promise((resolve) => requestAnimationFrame(resolve))\n  }\n}\n"
  },
  {
    "path": "src/tests/cases/index.ts",
    "content": "export * from \"./application_test_case\"\nexport * from \"./controller_test_case\"\nexport * from \"./dom_test_case\"\nexport * from \"./log_controller_test_case\"\nexport * from \"./observer_test_case\"\nexport * from \"./test_case\"\n"
  },
  {
    "path": "src/tests/cases/log_controller_test_case.ts",
    "content": "import { ControllerTestCase } from \"./controller_test_case\"\nimport { LogController, ActionLogEntry } from \"../controllers/log_controller\"\nimport { ControllerConstructor } from \"../../core/controller\"\n\nexport class LogControllerTestCase extends ControllerTestCase(LogController) {\n  controllerConstructor!: ControllerConstructor & { actionLog: ActionLogEntry[] }\n\n  async setup() {\n    this.controllerConstructor.actionLog = []\n    await super.setup()\n  }\n\n  assertActions(...actions: any[]) {\n    this.assert.equal(this.actionLog.length, actions.length)\n\n    actions.forEach((expected, index) => {\n      const keys = Object.keys(expected)\n      const actual = slice(this.actionLog[index] || {}, keys)\n      const result = keys.every((key) => deepEqual(expected[key], actual[key]))\n      this.assert.pushResult({ result, actual, expected, message: \"\" })\n    })\n  }\n\n  assertNoActions() {\n    this.assert.equal(this.actionLog.length, 0)\n  }\n\n  get actionLog(): ActionLogEntry[] {\n    return this.controllerConstructor.actionLog\n  }\n}\n\nfunction slice(object: any, keys: string[]): any {\n  return keys.reduce((result: any, key: string) => ((result[key] = object[key]), result), {})\n}\n\nfunction deepEqual(obj1: any, obj2: any): boolean {\n  if (obj1 === obj2) {\n    return true\n  } else if (typeof obj1 === \"object\" && typeof obj2 === \"object\") {\n    if (Object.keys(obj1).length !== Object.keys(obj2).length) {\n      return false\n    }\n    for (const prop in obj1) {\n      if (!deepEqual(obj1[prop], obj2[prop])) {\n        return false\n      }\n    }\n    return true\n  } else {\n    return false\n  }\n}\n"
  },
  {
    "path": "src/tests/cases/observer_test_case.ts",
    "content": "import { DOMTestCase } from \"./dom_test_case\"\n\nexport interface Observer {\n  start(): void\n  stop(): void\n}\n\nexport class ObserverTestCase extends DOMTestCase {\n  observer!: Observer\n  calls: any[][] = []\n  private setupCallCount = 0\n\n  async setup() {\n    this.observer.start()\n    await this.nextFrame\n    this.setupCallCount = this.calls.length\n  }\n\n  async teardown() {\n    this.observer.stop()\n  }\n\n  get testCalls() {\n    return this.calls.slice(this.setupCallCount)\n  }\n\n  recordCall(methodName: string, ...args: any[]) {\n    this.calls.push([methodName, ...args])\n  }\n}\n"
  },
  {
    "path": "src/tests/cases/test_case.ts",
    "content": "export class TestCase {\n  readonly assert: Assert\n\n  static defineModule(moduleName: string = this.name, qUnit: QUnit = QUnit) {\n    qUnit.module(moduleName, (_hooks) => {\n      this.manifest.forEach(([type, name]) => {\n        type = this.shouldSkipTest(name) ? \"skip\" : type\n        const method = (qUnit as any)[type] as Function\n        const test = this.getTest(name)\n        method.call(qUnit, name, test)\n      })\n    })\n  }\n\n  static getTest(testName: string) {\n    return async (assert: Assert) => this.runTest(testName, assert)\n  }\n\n  static runTest(testName: string, assert: Assert) {\n    const testCase = new this(assert)\n    return testCase.runTest(testName)\n  }\n\n  static shouldSkipTest(_testName: string): boolean {\n    return false\n  }\n\n  static get manifest() {\n    return this.testPropertyNames.map((name) => [name.slice(0, 4), name.slice(5)])\n  }\n\n  static get testNames(): string[] {\n    return this.manifest.map(([_type, name]) => name)\n  }\n\n  static get testPropertyNames(): string[] {\n    return Object.keys(this.prototype).filter((name) => name.match(/^(skip|test|todo) /))\n  }\n\n  constructor(assert: Assert) {\n    this.assert = assert\n  }\n\n  async runTest(testName: string) {\n    try {\n      await this.setup()\n      await this.runTestBody(testName)\n    } finally {\n      await this.teardown()\n    }\n  }\n\n  async runTestBody(testName: string) {\n    const testCase = (this as any)[`test ${testName}`] || (this as any)[`todo ${testName}`]\n    if (typeof testCase == \"function\") {\n      return testCase.call(this)\n    } else {\n      return Promise.reject(`test not found: \"${testName}\"`)\n    }\n  }\n\n  async setup() {\n    // Override this method in your subclass to prepare your test environment\n  }\n\n  async teardown() {\n    // Override this method in your subclass to clean up your test environment\n  }\n}\n"
  },
  {
    "path": "src/tests/controllers/class_controller.ts",
    "content": "import { Controller } from \"../../core/controller\"\n\nclass BaseClassController extends Controller {\n  static classes = [\"active\"]\n\n  readonly activeClass!: string\n  readonly activeClasses!: string[]\n  readonly hasActiveClass!: boolean\n}\n\nexport class ClassController extends BaseClassController {\n  static classes = [\"enabled\", \"loading\", \"success\"]\n\n  readonly hasEnabledClass!: boolean\n  readonly enabledClass!: string\n  readonly enabledClasses!: string[]\n  readonly loadingClass!: string\n  readonly successClass!: string\n  readonly successClasses!: string[]\n}\n"
  },
  {
    "path": "src/tests/controllers/default_value_controller.ts",
    "content": "import { Controller } from \"../../core/controller\"\nimport { ValueDefinitionMap, ValueDescriptorMap } from \"../../core/value_properties\"\n\nexport class DefaultValueController extends Controller {\n  static values: ValueDefinitionMap = {\n    defaultBoolean: false,\n    defaultBooleanTrue: { type: Boolean, default: true },\n    defaultBooleanFalse: { type: Boolean, default: false },\n    defaultBooleanOverride: true,\n\n    defaultString: \"\",\n    defaultStringHello: { type: String, default: \"Hello\" },\n    defaultStringOverride: \"Override me\",\n\n    defaultNumber: 0,\n    defaultNumberThousand: { type: Number, default: 1000 },\n    defaultNumberZero: { type: Number, default: 0 },\n    defaultNumberOverride: 9999,\n\n    defaultArray: [],\n    defaultArrayFilled: { type: Array, default: [1, 2, 3] },\n    defaultArrayOverride: [9, 9, 9],\n\n    defaultObject: {},\n    defaultObjectPerson: { type: Object, default: { name: \"David\" } },\n    defaultObjectOverride: { override: \"me\" },\n  }\n\n  valueDescriptorMap!: ValueDescriptorMap\n\n  defaultBooleanValue!: boolean\n  hasDefaultBooleanValue!: boolean\n  defaultBooleanTrueValue!: boolean\n  defaultBooleanFalseValue!: boolean\n  hasDefaultBooleanTrueValue!: boolean\n  hasDefaultBooleanFalseValue!: boolean\n  defaultBooleanOverrideValue!: boolean\n  hasDefaultBooleanOverrideValue!: boolean\n\n  defaultStringValue!: string\n  hasDefaultStringValue!: boolean\n  defaultStringHelloValue!: string\n  hasDefaultStringHelloValue!: boolean\n  defaultStringOverrideValue!: string\n  hasDefaultStringOverrideValue!: boolean\n\n  defaultNumberValue!: number\n  hasDefaultNumberValue!: boolean\n  defaultNumberThousandValue!: number\n  hasDefaultNumberThousandValue!: boolean\n  defaultNumberZeroValue!: number\n  hasDefaultNumberZeroValue!: boolean\n  defaultNumberOverrideValue!: number\n  hasDefaultNumberOverrideValue!: boolean\n\n  defaultArrayValue!: any[]\n  hasDefaultArrayValue!: boolean\n  defaultArrayFilledValue!: { [key: string]: any }\n  hasDefaultArrayFilledValue!: boolean\n  defaultArrayOverrideValue!: { [key: string]: any }\n  hasDefaultArrayOverrideValue!: boolean\n\n  defaultObjectValue!: object\n  hasDefaultObjectValue!: boolean\n  defaultObjectPersonValue!: object\n  hasDefaultObjectPersonValue!: boolean\n  defaultObjectOverrideValue!: object\n  hasDefaultObjectOverrideValue!: boolean\n  lifecycleCallbacks: string[] = []\n\n  initialize() {\n    this.lifecycleCallbacks.push(\"initialize\")\n  }\n\n  connect() {\n    this.lifecycleCallbacks.push(\"connect\")\n  }\n\n  defaultBooleanValueChanged() {\n    this.lifecycleCallbacks.push(\"defaultBooleanValueChanged\")\n  }\n}\n"
  },
  {
    "path": "src/tests/controllers/log_controller.ts",
    "content": "import { ActionEvent } from \"../../core/action_event\"\nimport { Controller } from \"../../core/controller\"\n\nexport type ActionLogEntry = {\n  name: string\n  controller: Controller\n  identifier: string\n  eventType: string\n  currentTarget: EventTarget | null\n  params: { [key: string]: any }\n  defaultPrevented: boolean\n  passive: boolean\n}\n\nexport class LogController extends Controller {\n  static actionLog: ActionLogEntry[] = []\n  initializeCount = 0\n  connectCount = 0\n  disconnectCount = 0\n\n  initialize() {\n    this.initializeCount++\n  }\n\n  connect() {\n    this.connectCount++\n  }\n\n  disconnect() {\n    this.disconnectCount++\n  }\n\n  log(event: ActionEvent) {\n    this.recordAction(\"log\", event)\n  }\n\n  log2(event: ActionEvent) {\n    this.recordAction(\"log2\", event)\n  }\n\n  log3(event: ActionEvent) {\n    this.recordAction(\"log3\", event)\n  }\n\n  logPassive(event: ActionEvent) {\n    event.preventDefault()\n    if (event.defaultPrevented) {\n      this.recordAction(\"logPassive\", event, false)\n    } else {\n      this.recordAction(\"logPassive\", event, true)\n    }\n  }\n\n  stop(event: ActionEvent) {\n    this.recordAction(\"stop\", event)\n    event.stopImmediatePropagation()\n  }\n\n  get actionLog() {\n    return (this.constructor as typeof LogController).actionLog\n  }\n\n  private recordAction(name: string, event: ActionEvent, passive?: boolean) {\n    this.actionLog.push({\n      name,\n      controller: this,\n      identifier: this.identifier,\n      eventType: event.type,\n      currentTarget: event.currentTarget,\n      params: event.params,\n      defaultPrevented: event.defaultPrevented,\n      passive: passive || false,\n    })\n  }\n}\n"
  },
  {
    "path": "src/tests/controllers/outlet_controller.ts",
    "content": "import { Controller } from \"../../core/controller\"\n\nclass BaseOutletController extends Controller {\n  static outlets = [\"alpha\"]\n\n  alphaOutlet!: Controller | null\n  alphaOutlets!: Controller[]\n  alphaOutletElement!: Element | null\n  alphaOutletElements!: Element[]\n  hasAlphaOutlet!: boolean\n}\n\nexport class OutletController extends BaseOutletController {\n  static classes = [\"connected\", \"disconnected\"]\n  static outlets = [\"beta\", \"gamma\", \"delta\", \"omega\", \"namespaced--epsilon\"]\n\n  static values = {\n    alphaOutletConnectedCallCount: Number,\n    alphaOutletDisconnectedCallCount: Number,\n    betaOutletConnectedCallCount: Number,\n    betaOutletDisconnectedCallCount: Number,\n    betaOutletsInConnect: Number,\n    gammaOutletConnectedCallCount: Number,\n    gammaOutletDisconnectedCallCount: Number,\n    namespacedEpsilonOutletConnectedCallCount: Number,\n    namespacedEpsilonOutletDisconnectedCallCount: Number,\n  }\n\n  betaOutlet!: Controller | null\n  betaOutlets!: Controller[]\n  betaOutletElement!: Element | null\n  betaOutletElements!: Element[]\n  hasBetaOutlet!: boolean\n\n  namespacedEpsilonOutlet!: Controller | null\n  namespacedEpsilonOutlets!: Controller[]\n  namespacedEpsilonOutletElement!: Element | null\n  namespacedEpsilonOutletElements!: Element[]\n  hasNamespacedEpsilonOutlet!: boolean\n\n  hasConnectedClass!: boolean\n  hasDisconnectedClass!: boolean\n  connectedClass!: string\n  disconnectedClass!: string\n\n  alphaOutletConnectedCallCountValue = 0\n  alphaOutletDisconnectedCallCountValue = 0\n  betaOutletConnectedCallCountValue = 0\n  betaOutletDisconnectedCallCountValue = 0\n  betaOutletsInConnectValue = 0\n  gammaOutletConnectedCallCountValue = 0\n  gammaOutletDisconnectedCallCountValue = 0\n  namespacedEpsilonOutletConnectedCallCountValue = 0\n  namespacedEpsilonOutletDisconnectedCallCountValue = 0\n\n  connect() {\n    this.betaOutletsInConnectValue = this.betaOutlets.length\n  }\n\n  alphaOutletConnected(_outlet: Controller, element: Element) {\n    if (this.hasConnectedClass) element.classList.add(this.connectedClass)\n    this.alphaOutletConnectedCallCountValue++\n  }\n\n  alphaOutletDisconnected(_outlet: Controller, element: Element) {\n    if (this.hasDisconnectedClass) element.classList.add(this.disconnectedClass)\n    this.alphaOutletDisconnectedCallCountValue++\n  }\n\n  betaOutletConnected(_outlet: Controller, element: Element) {\n    if (this.hasConnectedClass) element.classList.add(this.connectedClass)\n    this.betaOutletConnectedCallCountValue++\n  }\n\n  betaOutletDisconnected(_outlet: Controller, element: Element) {\n    if (this.hasDisconnectedClass) element.classList.add(this.disconnectedClass)\n    this.betaOutletDisconnectedCallCountValue++\n  }\n\n  gammaOutletConnected(_outlet: Controller, element: Element) {\n    if (this.hasConnectedClass) element.classList.add(this.connectedClass)\n    this.gammaOutletConnectedCallCountValue++\n  }\n\n  namespacedEpsilonOutletConnected(_outlet: Controller, element: Element) {\n    if (this.hasConnectedClass) element.classList.add(this.connectedClass)\n    this.namespacedEpsilonOutletConnectedCallCountValue++\n  }\n\n  namespacedEpsilonOutletDisconnected(_outlet: Controller, element: Element) {\n    if (this.hasDisconnectedClass) element.classList.add(this.disconnectedClass)\n    this.namespacedEpsilonOutletDisconnectedCallCountValue++\n  }\n}\n"
  },
  {
    "path": "src/tests/controllers/target_controller.ts",
    "content": "import { Controller } from \"../../core/controller\"\n\nclass BaseTargetController extends Controller {\n  static targets = [\"alpha\"]\n\n  alphaTarget!: Element | null\n  alphaTargets!: Element[]\n  hasAlphaTarget!: boolean\n}\n\nexport class TargetController extends BaseTargetController {\n  static classes = [\"connected\", \"disconnected\"]\n  static targets = [\"beta\", \"input\", \"recursive\"]\n  static values = {\n    inputTargetConnectedCallCount: Number,\n    inputTargetDisconnectedCallCount: Number,\n    recursiveTargetConnectedCallCount: Number,\n    recursiveTargetDisconnectedCallCount: Number,\n  }\n\n  betaTarget!: Element | null\n  betaTargets!: Element[]\n  hasBetaTarget!: boolean\n\n  inputTarget!: Element | null\n  inputTargets!: Element[]\n  hasInputTarget!: boolean\n\n  hasConnectedClass!: boolean\n  hasDisconnectedClass!: boolean\n  connectedClass!: string\n  disconnectedClass!: string\n\n  inputTargetConnectedCallCountValue = 0\n  inputTargetDisconnectedCallCountValue = 0\n  recursiveTargetConnectedCallCountValue = 0\n  recursiveTargetDisconnectedCallCountValue = 0\n\n  inputTargetConnected(element: Element) {\n    if (this.hasConnectedClass) element.classList.add(this.connectedClass)\n    this.inputTargetConnectedCallCountValue++\n  }\n\n  inputTargetDisconnected(element: Element) {\n    if (this.hasDisconnectedClass) element.classList.add(this.disconnectedClass)\n    this.inputTargetDisconnectedCallCountValue++\n  }\n\n  recursiveTargetConnected(element: Element) {\n    element.remove()\n\n    this.recursiveTargetConnectedCallCountValue++\n    this.element.append(element)\n  }\n\n  recursiveTargetDisconnected(_element: Element) {\n    this.recursiveTargetDisconnectedCallCountValue++\n  }\n}\n"
  },
  {
    "path": "src/tests/controllers/value_controller.ts",
    "content": "import { Controller } from \"../../core/controller\"\nimport { ValueDefinitionMap, ValueDescriptorMap } from \"../../core/value_properties\"\n\nclass BaseValueController extends Controller {\n  static values: ValueDefinitionMap = {\n    shadowedBoolean: String,\n    string: String,\n    numeric: Number,\n  }\n\n  valueDescriptorMap!: ValueDescriptorMap\n  stringValue!: string\n  numericValue!: number\n}\n\nexport class ValueController extends BaseValueController {\n  static values: ValueDefinitionMap = {\n    shadowedBoolean: Boolean,\n    missingString: String,\n    ids: Array,\n    options: Object,\n    \"time-24hr\": Boolean,\n  }\n\n  shadowedBooleanValue!: boolean\n  missingStringValue!: string\n  idsValue!: any[]\n  optionsValue!: { [key: string]: any }\n  time24hrValue!: boolean\n\n  loggedNumericValues: number[] = []\n  oldLoggedNumericValues: any[] = []\n  numericValueChanged(value: number, oldValue: any) {\n    this.loggedNumericValues.push(value)\n    this.oldLoggedNumericValues.push(oldValue)\n  }\n\n  loggedMissingStringValues: string[] = []\n  oldLoggedMissingStringValues: any[] = []\n  missingStringValueChanged(value: string, oldValue: any) {\n    this.loggedMissingStringValues.push(value)\n    this.oldLoggedMissingStringValues.push(oldValue)\n  }\n\n  optionsValues: Object[] = []\n  oldOptionsValues: any[] = []\n  optionsValueChanged(value: Object, oldValue: any) {\n    this.optionsValues.push(value)\n    this.oldOptionsValues.push(oldValue)\n  }\n}\n"
  },
  {
    "path": "src/tests/fixtures/application_start/helpers.ts",
    "content": "import { Application, Controller } from \"../../../core\"\n\nexport function startApplication() {\n  const startState = document.readyState\n\n  class PostMessageController extends Controller {\n    itemTargets!: Element[]\n\n    static targets = [\"item\"]\n\n    connect() {\n      const connectState = document.readyState\n      const targetCount = this.itemTargets.length\n      const message = JSON.stringify({ startState, connectState, targetCount })\n      parent.postMessage(message, location.origin)\n    }\n  }\n\n  const application = Application.start()\n  application.register(\"a\", PostMessageController)\n}\n"
  },
  {
    "path": "src/tests/fixtures/application_start/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <script src=\"/base/dist/tests/fixtures/application_start/index.js\"></script>\n</head>\n<body>\n  <div data-controller=\"a\">\n    <span data-a-target=\"item\"></span>\n    <span data-a-target=\"item\"></span>\n    <script>\n      // If \"a\" controller is loaded while the document is loading,\n      // synchronously accessing `this.itemTargets` will only return\n      // the first two target elements. The third hasn't been parsed\n      // yet because the browser's still working through this script.\n      // See: https://github.com/hotwired/stimulus/issues/97\n      1 + 1\n    </script>\n    <span data-a-target=\"item\"></span>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/application_start/index.ts",
    "content": "import { startApplication } from \"./helpers\"\n\nstartApplication()\naddEventListener(\"DOMContentLoaded\", startApplication)\naddEventListener(\"load\", startApplication)\n"
  },
  {
    "path": "src/tests/index.ts",
    "content": "const context = require.context(\"./modules\", true, /\\.js$/)\nconst modules = context.keys().map((key) => context(key).default)\nmodules.forEach((constructor) => constructor.defineModule())\n"
  },
  {
    "path": "src/tests/modules/core/action_click_filter_tests.ts",
    "content": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionClickFilterTests extends LogControllerTestCase {\n  identifier = [\"a\"]\n\n  fixtureHTML = `\n    <div data-controller=\"a\">\n      <button id=\"ctrl\" data-action=\"click->a#log ctrl+click->a#log2 meta+click->a#log3\"></button>\n    </div>\n  `\n\n  async \"test ignoring clicks with unmatched modifier\"() {\n    const button = this.findElement(\"#ctrl\")\n    await this.triggerMouseEvent(button, \"click\", { ctrlKey: true })\n    await this.nextFrame\n    this.assertActions(\n      { name: \"log\", identifier: \"a\", eventType: \"click\", currentTarget: button },\n      { name: \"log2\", identifier: \"a\", eventType: \"click\", currentTarget: button }\n    )\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/action_keyboard_filter_tests.ts",
    "content": "import { TestApplication } from \"../../cases/application_test_case\"\nimport { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\nimport { Schema, defaultSchema } from \"../../../core/schema\"\nimport { Application } from \"../../../core/application\"\n\nconst customSchema = { ...defaultSchema, keyMappings: { ...defaultSchema.keyMappings, a: \"a\", b: \"b\" } }\n\nexport default class ActionKeyboardFilterTests extends LogControllerTestCase {\n  schema: Schema = customSchema\n  application: Application = new TestApplication(this.fixtureElement, this.schema)\n\n  identifier = [\"a\"]\n  fixtureHTML = `\n    <div data-controller=\"a\" data-action=\"keydown.esc@document->a#log\">\n      <button id=\"button1\" data-action=\"keydown.enter->a#log keydown.space->a#log2 keydown->a#log3\"></button>\n      <button id=\"button2\" data-action=\"keydown.tab->a#log   keydown.esc->a#log2   keydown->a#log3\"></button>\n      <button id=\"button3\" data-action=\"keydown.up->a#log    keydown.down->a#log2  keydown->a#log3\"></button>\n      <button id=\"button4\" data-action=\"keydown.left->a#log  keydown.right->a#log2 keydown->a#log3\"></button>\n      <button id=\"button5\" data-action=\"keydown.home->a#log  keydown.end->a#log2   keydown->a#log3\"></button>\n      <button id=\"button6\" data-action=\"keyup.end->a#log     keyup->a#log3\"></button>\n      <button id=\"button7\"></button>\n      <button id=\"button8\" data-action=\"keydown.a->a#log keydown.b->a#log2\"></button>\n      <button id=\"button9\" data-action=\"keydown.shift+a->a#log keydown.a->a#log2 keydown.ctrl+shift+a->a#log3\">\n      <button id=\"button10\" data-action=\"jquery.custom.event->a#log jquery.a->a#log2\">\n    </div>\n  `\n\n  async \"test ignore event handlers associated with modifiers other than Enter\"() {\n    const button = this.findElement(\"#button1\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"Enter\" })\n    this.assertActions(\n      { name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than Space\"() {\n    const button = this.findElement(\"#button1\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \" \" })\n    this.assertActions(\n      { name: \"log2\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than Tab\"() {\n    const button = this.findElement(\"#button2\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"Tab\" })\n    this.assertActions(\n      { name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than Escape\"() {\n    const button = this.findElement(\"#button2\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"Escape\" })\n    this.assertActions(\n      { name: \"log2\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than ArrowUp\"() {\n    const button = this.findElement(\"#button3\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"ArrowUp\" })\n    this.assertActions(\n      { name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than ArrowDown\"() {\n    const button = this.findElement(\"#button3\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"ArrowDown\" })\n    this.assertActions(\n      { name: \"log2\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than ArrowLeft\"() {\n    const button = this.findElement(\"#button4\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"ArrowLeft\" })\n    this.assertActions(\n      { name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than ArrowRight\"() {\n    const button = this.findElement(\"#button4\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"ArrowRight\" })\n    this.assertActions(\n      { name: \"log2\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than Home\"() {\n    const button = this.findElement(\"#button5\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"Home\" })\n    this.assertActions(\n      { name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test ignore event handlers associated with modifiers other than End\"() {\n    const button = this.findElement(\"#button5\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"End\" })\n    this.assertActions(\n      { name: \"log2\", identifier: \"a\", eventType: \"keydown\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button }\n    )\n  }\n\n  async \"test keyup\"() {\n    const button = this.findElement(\"#button6\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keyup\", { key: \"End\" })\n    this.assertActions(\n      { name: \"log\", identifier: \"a\", eventType: \"keyup\", currentTarget: button },\n      { name: \"log3\", identifier: \"a\", eventType: \"keyup\", currentTarget: button }\n    )\n  }\n\n  async \"test global event\"() {\n    const button = this.findElement(\"#button7\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"Escape\", bubbles: true })\n    this.assertActions({ name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: document })\n  }\n\n  async \"test custom keymapping: a\"() {\n    const button = this.findElement(\"#button8\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"a\" })\n    this.assertActions({ name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: button })\n  }\n\n  async \"test custom keymapping: b\"() {\n    const button = this.findElement(\"#button8\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"b\" })\n    this.assertActions({ name: \"log2\", identifier: \"a\", eventType: \"keydown\", currentTarget: button })\n  }\n\n  async \"test custom keymapping: unknown c\"() {\n    const button = this.findElement(\"#button8\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"c\" })\n    this.assertActions()\n  }\n\n  async \"test ignore event handlers associated with modifiers other than shift+a\"() {\n    const button = this.findElement(\"#button9\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"A\", shiftKey: true })\n    this.assertActions({ name: \"log\", identifier: \"a\", eventType: \"keydown\", currentTarget: button })\n  }\n\n  async \"test ignore event handlers associated with modifiers other than a\"() {\n    const button = this.findElement(\"#button9\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"a\" })\n    this.assertActions({ name: \"log2\", identifier: \"a\", eventType: \"keydown\", currentTarget: button })\n  }\n\n  async \"test ignore event handlers associated with modifiers other than ctrol+shift+a\"() {\n    const button = this.findElement(\"#button9\")\n    await this.nextFrame\n    await this.triggerKeyboardEvent(button, \"keydown\", { key: \"A\", ctrlKey: true, shiftKey: true })\n    this.assertActions({ name: \"log3\", identifier: \"a\", eventType: \"keydown\", currentTarget: button })\n  }\n\n  async \"test ignore filter syntax when not a keyboard event\"() {\n    const button = this.findElement(\"#button10\")\n    await this.nextFrame\n    await this.triggerEvent(button, \"jquery.custom.event\")\n    this.assertActions({ name: \"log\", identifier: \"a\", eventType: \"jquery.custom.event\", currentTarget: button })\n  }\n\n  async \"test ignore filter syntax when not a keyboard event (case2)\"() {\n    const button = this.findElement(\"#button10\")\n    await this.nextFrame\n    await this.triggerEvent(button, \"jquery.a\")\n    this.assertActions({ name: \"log2\", identifier: \"a\", eventType: \"jquery.a\", currentTarget: button })\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/action_ordering_tests.ts",
    "content": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionOrderingTests extends LogControllerTestCase {\n  identifier = [\"c\", \"d\"]\n  fixtureHTML = `\n    <div data-controller=\"c d\" data-action=\"click->c#log\">\n      <button data-action=\"c#log d#log2\"></button>\n    </div>\n  `\n\n  async \"test adding an action to the right\"() {\n    this.actionValue = \"c#log d#log2 c#log3\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log3\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.element }\n    )\n  }\n\n  async \"test adding an action to the left\"() {\n    this.actionValue = \"c#log3 c#log d#log2\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log3\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.element }\n    )\n  }\n\n  async \"test removing an action from the right\"() {\n    this.actionValue = \"c#log d#log2\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.element }\n    )\n  }\n\n  async \"test removing an action from the left\"() {\n    this.actionValue = \"d#log2 c#log3\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log3\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.element }\n    )\n  }\n\n  async \"test replacing an action on the left\"() {\n    this.actionValue = \"d#log2 c#log3\"\n    await this.nextFrame\n    this.actionValue = \"c#log d#log2 c#log3\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log3\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.element }\n    )\n  }\n\n  async \"test stopping an action\"() {\n    this.actionValue = \"c#log d#stop c#log3\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"stop\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement }\n    )\n  }\n\n  async \"test disconnecting a controller disconnects its actions\"() {\n    this.controllerValue = \"c\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.element }\n    )\n  }\n\n  set controllerValue(value: string) {\n    this.element.setAttribute(\"data-controller\", value)\n  }\n\n  set actionValue(value: string) {\n    this.buttonElement.setAttribute(\"data-action\", value)\n  }\n\n  get element() {\n    return this.findElement(\"div\")\n  }\n\n  get buttonElement() {\n    return this.findElement(\"button\")\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/action_params_case_insensitive_tests.ts",
    "content": "import ActionParamsTests from \"./action_params_tests\"\n\nexport default class ActionParamsCaseInsensitiveTests extends ActionParamsTests {\n  identifier = [\"CamelCase\", \"AnotherOne\"]\n  fixtureHTML = `\n    <div data-controller=\"CamelCase AnotherOne\">\n      <button data-CamelCase-id-param=\"123\"\n              data-CamelCase-multi-word-example-param=\"/path\"\n              data-CamelCase-active-param=\"true\"\n              data-CamelCase-inactive-param=\"false\"\n              data-CamelCase-empty-param=\"\"\n              data-CamelCase-payload-param='${JSON.stringify({ value: 1 })}'\n              data-CamelCase-param-something=\"not-reported\"\n              data-Something-param=\"not-reported\"\n              data-AnotherOne-id-param=\"234\">\n        <div id=\"nested\"></div>\n      </button>\n    </div>\n    <div id=\"outside\"></div>\n  `\n  expectedParamsForCamelCase = {\n    id: 123,\n    multiWordExample: \"/path\",\n    payload: {\n      value: 1,\n    },\n    active: true,\n    empty: \"\",\n    inactive: false,\n  }\n\n  async \"test clicking on the element does return its params\"() {\n    this.actionValue = \"click->CamelCase#log\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ identifier: \"CamelCase\", params: this.expectedParamsForCamelCase })\n  }\n\n  async \"test global event return element params where the action is defined\"() {\n    this.actionValue = \"keydown@window->CamelCase#log\"\n    await this.nextFrame\n    await this.triggerEvent(\"#outside\", \"keydown\")\n\n    this.assertActions({ identifier: \"CamelCase\", params: this.expectedParamsForCamelCase })\n  }\n\n  async \"test passing params to namespaced controller\"() {\n    this.actionValue = \"click->CamelCase#log click->AnotherOne#log2\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { identifier: \"CamelCase\", params: this.expectedParamsForCamelCase },\n      { identifier: \"AnotherOne\", params: { id: 234 } }\n    )\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/action_params_tests.ts",
    "content": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionParamsTests extends LogControllerTestCase {\n  identifier = [\"c\", \"d\"]\n  fixtureHTML = `\n    <div data-controller=\"c d\">\n      <button data-c-id-param=\"123\"\n              data-c-multi-word-example-param=\"/path\"\n              data-c-active-param=\"true\"\n              data-c-inactive-param=\"false\"\n              data-c-empty-param=\"\"\n              data-c-payload-param='${JSON.stringify({ value: 1 })}'\n              data-c-param-something=\"not-reported\"\n              data-something-param=\"not-reported\"\n              data-d-id-param=\"234\">\n        <div id=\"nested\"></div>\n      </button>\n    </div>\n    <div id=\"outside\"></div>\n  `\n  expectedParamsForC = {\n    id: 123,\n    multiWordExample: \"/path\",\n    payload: {\n      value: 1,\n    },\n    active: true,\n    empty: \"\",\n    inactive: false,\n  }\n\n  async \"test clicking on the element does return its params\"() {\n    this.actionValue = \"click->c#log\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ identifier: \"c\", params: this.expectedParamsForC })\n  }\n\n  async \"test global event return element params where the action is defined\"() {\n    this.actionValue = \"keydown@window->c#log\"\n    await this.nextFrame\n    await this.triggerEvent(\"#outside\", \"keydown\")\n\n    this.assertActions({ identifier: \"c\", params: this.expectedParamsForC })\n  }\n\n  async \"test passing params to namespaced controller\"() {\n    this.actionValue = \"click->c#log click->d#log2\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ identifier: \"c\", params: this.expectedParamsForC }, { identifier: \"d\", params: { id: 234 } })\n  }\n\n  async \"test updating manually the params values\"() {\n    this.actionValue = \"click->c#log\"\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ identifier: \"c\", params: this.expectedParamsForC })\n\n    this.buttonElement.setAttribute(\"data-c-id-param\", \"234\")\n    this.buttonElement.setAttribute(\"data-c-new-param\", \"new\")\n    this.buttonElement.removeAttribute(\"data-c-payload-param\")\n    this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { identifier: \"c\", params: this.expectedParamsForC },\n      {\n        identifier: \"c\",\n        params: {\n          id: 234,\n          new: \"new\",\n          multiWordExample: \"/path\",\n          active: true,\n          empty: \"\",\n          inactive: false,\n        },\n      }\n    )\n  }\n\n  async \"test clicking on a nested element does return the params of the actionable element\"() {\n    this.actionValue = \"click->c#log\"\n    await this.nextFrame\n    await this.triggerEvent(this.nestedElement, \"click\")\n\n    this.assertActions({ identifier: \"c\", params: this.expectedParamsForC })\n  }\n\n  set actionValue(value: string) {\n    this.buttonElement.setAttribute(\"data-action\", value)\n  }\n\n  get element() {\n    return this.findElement(\"div\")\n  }\n\n  get buttonElement() {\n    return this.findElement(\"button\")\n  }\n\n  get nestedElement() {\n    return this.findElement(\"#nested\")\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/action_tests.ts",
    "content": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionTests extends LogControllerTestCase {\n  identifier = \"c\"\n  fixtureHTML = `\n    <div data-controller=\"c\" data-action=\"keydown@window->c#log\">\n      <button data-action=\"c#log\"><span>Log</span></button>\n      <div id=\"outer\" data-action=\"click->c#log\">\n        <div id=\"inner\" data-controller=\"c\" data-action=\"click->c#log keyup@window->c#log\"></div>\n      </div>\n      <div id=\"multiple\" data-action=\"click->c#log click->c#log2 mousedown->c#log\"></div>\n    </div>\n    <div id=\"outside\"></div>\n    <svg id=\"svgRoot\" data-controller=\"c\" data-action=\"click->c#log\">\n      <circle id=\"svgChild\" data-action=\"mousedown->c#log\" cx=\"5\" cy=\"5\" r=\"5\">\n    </svg>\n  `\n\n  async \"test default event\"() {\n    await this.triggerEvent(\"button\", \"click\")\n    this.assertActions({ name: \"log\", eventType: \"click\" })\n  }\n\n  async \"test bubbling events\"() {\n    await this.triggerEvent(\"span\", \"click\")\n    this.assertActions({ eventType: \"click\", currentTarget: this.findElement(\"button\") })\n  }\n\n  async \"test non-bubbling events\"() {\n    await this.triggerEvent(\"span\", \"click\", { bubbles: false })\n    this.assertNoActions()\n    await this.triggerEvent(\"button\", \"click\", { bubbles: false })\n    this.assertActions({ eventType: \"click\" })\n  }\n\n  async \"test nested actions\"() {\n    const innerController = this.controllers[1]\n    await this.triggerEvent(\"#inner\", \"click\")\n    this.assert.ok(true)\n    this.assertActions({ controller: innerController, eventType: \"click\" })\n  }\n\n  async \"test global actions\"() {\n    await this.triggerEvent(\"#outside\", \"keydown\")\n    this.assertActions({ name: \"log\", eventType: \"keydown\" })\n  }\n\n  async \"test nested global actions\"() {\n    const innerController = this.controllers[1]\n    await this.triggerEvent(\"#outside\", \"keyup\")\n    this.assertActions({ controller: innerController, eventType: \"keyup\" })\n  }\n\n  async \"test multiple actions\"() {\n    await this.triggerEvent(\"#multiple\", \"mousedown\")\n    await this.triggerEvent(\"#multiple\", \"click\")\n    this.assertActions(\n      { name: \"log\", eventType: \"mousedown\" },\n      { name: \"log\", eventType: \"click\" },\n      { name: \"log2\", eventType: \"click\" }\n    )\n  }\n\n  async \"test actions on svg elements\"() {\n    await this.triggerEvent(\"#svgRoot\", \"click\")\n    await this.triggerEvent(\"#svgChild\", \"mousedown\")\n    this.assertActions({ name: \"log\", eventType: \"click\" }, { name: \"log\", eventType: \"mousedown\" })\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/action_timing_tests.ts",
    "content": "import { Controller } from \"../../../core/controller\"\nimport { ControllerTestCase } from \"../../cases/controller_test_case\"\n\nclass ActionTimingController extends Controller {\n  static targets = [\"button\"]\n  buttonTarget!: HTMLButtonElement\n  event?: Event\n\n  connect() {\n    this.buttonTarget.click()\n  }\n\n  record(event: Event) {\n    this.event = event\n  }\n}\n\nexport default class ActionTimingTests extends ControllerTestCase(ActionTimingController) {\n  controllerConstructor = ActionTimingController\n  identifier = \"c\"\n  fixtureHTML = `\n    <div data-controller=\"c\">\n      <button data-c-target=\"button\" data-action=\"c#record\">Log</button>\n    </div>\n  `\n\n  async \"test triggering an action on connect\"() {\n    const { event } = this.controller\n    this.assert.ok(event)\n    this.assert.equal(event && event.type, \"click\")\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/application_start_tests.ts",
    "content": "import { DOMTestCase } from \"../../cases\"\n\nexport default class ApplicationStartTests extends DOMTestCase {\n  iframe!: HTMLIFrameElement\n\n  async setup() {\n    this.iframe = document.createElement(\"iframe\")\n    this.iframe.src = \"/base/src/tests/fixtures/application_start/index.html\"\n    this.fixtureElement.appendChild(this.iframe)\n  }\n\n  async \"test starting an application when the document is loading\"() {\n    const message = await this.messageFromStartState(\"loading\")\n    this.assertIn(message.connectState, [\"interactive\", \"complete\"])\n    this.assert.equal(message.targetCount, 3)\n  }\n\n  async \"test starting an application when the document is interactive\"() {\n    const message = await this.messageFromStartState(\"interactive\")\n    this.assertIn(message.connectState, [\"interactive\", \"complete\"])\n    this.assert.equal(message.targetCount, 3)\n  }\n\n  async \"test starting an application when the document is complete\"() {\n    const message = await this.messageFromStartState(\"complete\")\n    this.assertIn(message.connectState, [\"complete\"])\n    this.assert.equal(message.targetCount, 3)\n  }\n\n  private messageFromStartState(startState: string): Promise<any> {\n    return new Promise((resolve) => {\n      const receiveMessage = (event: MessageEvent) => {\n        if (event.source == this.iframe.contentWindow) {\n          const message = JSON.parse(event.data)\n          if (message.startState == startState) {\n            removeEventListener(\"message\", receiveMessage)\n            resolve(message)\n          }\n        }\n      }\n      addEventListener(\"message\", receiveMessage)\n    })\n  }\n\n  private assertIn(actual: any, expected: any[]) {\n    const state = expected.indexOf(actual) > -1\n    const message = `${JSON.stringify(actual)} is not in ${JSON.stringify(expected)}`\n    this.assert.ok(state, message)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/application_tests.ts",
    "content": "import { ApplicationTestCase } from \"../../cases/application_test_case\"\nimport { LogController } from \"../../controllers/log_controller\"\n\nclass AController extends LogController {}\nclass BController extends LogController {}\n\nexport default class ApplicationTests extends ApplicationTestCase {\n  fixtureHTML = `<div data-controller=\"a\"><div data-controller=\"b\">`\n  private definitions = [\n    { controllerConstructor: AController, identifier: \"a\" },\n    { controllerConstructor: BController, identifier: \"b\" },\n  ]\n\n  async \"test Application#register\"() {\n    this.assert.equal(this.controllers.length, 0)\n    this.application.register(\"log\", LogController)\n    await this.renderFixture(`<div data-controller=\"log\">`)\n\n    this.assert.equal(this.controllers[0].initializeCount, 1)\n    this.assert.equal(this.controllers[0].connectCount, 1)\n  }\n\n  \"test Application#load\"() {\n    this.assert.equal(this.controllers.length, 0)\n    this.application.load(this.definitions)\n    this.assert.equal(this.controllers.length, 2)\n\n    this.assert.ok(this.controllers[0] instanceof AController)\n    this.assert.equal(this.controllers[0].initializeCount, 1)\n    this.assert.equal(this.controllers[0].connectCount, 1)\n\n    this.assert.ok(this.controllers[1] instanceof BController)\n    this.assert.equal(this.controllers[1].initializeCount, 1)\n    this.assert.equal(this.controllers[1].connectCount, 1)\n  }\n\n  \"test Application#unload\"() {\n    this.application.load(this.definitions)\n    const originalControllers = this.controllers\n\n    this.application.unload(\"a\")\n    this.assert.equal(originalControllers[0].disconnectCount, 1)\n\n    this.assert.equal(this.controllers.length, 1)\n    this.assert.ok(this.controllers[0] instanceof BController)\n  }\n\n  get controllers() {\n    return this.application.controllers as LogController[]\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/class_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { ClassController } from \"../../controllers/class_controller\"\n\nexport default class ClassTests extends ControllerTestCase(ClassController) {\n  fixtureHTML = `\n    <div data-controller=\"${this.identifier}\"\n      data-${this.identifier}-active-class=\"test--active\"\n      data-${this.identifier}-loading-class=\"busy\"\n      data-${this.identifier}-success-class=\"bg-green-400 border border-green-600\"\n      data-loading-class=\"xxx\"\n    ></div>\n  `\n\n  \"test accessing a class property\"() {\n    this.assert.ok(this.controller.hasActiveClass)\n    this.assert.equal(this.controller.activeClass, \"test--active\")\n    this.assert.deepEqual(this.controller.activeClasses, [\"test--active\"])\n  }\n\n  \"test accessing a missing class property throws an error\"() {\n    this.assert.notOk(this.controller.hasEnabledClass)\n    this.assert.raises(() => this.controller.enabledClass)\n    this.assert.equal(this.controller.enabledClasses.length, 0)\n  }\n\n  \"test classes must be scoped by identifier\"() {\n    this.assert.equal(this.controller.loadingClass, \"busy\")\n  }\n\n  \"test multiple classes map to array\"() {\n    this.assert.deepEqual(this.controller.successClasses, [\"bg-green-400\", \"border\", \"border-green-600\"])\n  }\n\n  \"test accessing a class property returns first class if multiple classes are used\"() {\n    this.assert.equal(this.controller.successClass, \"bg-green-400\")\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/data_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\n\nexport default class DataTests extends ControllerTestCase() {\n  fixtureHTML = `\n    <div data-controller=\"${this.identifier}\"\n      data-${this.identifier}-alpha=\"hello world\"\n      data-${this.identifier}-beta-gamma=\"123\">\n    </div>\n  `\n\n  \"test DataSet#get\"() {\n    this.assert.equal(this.controller.data.get(\"alpha\"), \"hello world\")\n    this.assert.equal(this.controller.data.get(\"betaGamma\"), \"123\")\n    this.assert.equal(this.controller.data.get(\"nonexistent\"), null)\n  }\n\n  \"test DataSet#set\"() {\n    this.assert.equal(this.controller.data.set(\"alpha\", \"ok\"), \"ok\")\n    this.assert.equal(this.controller.data.get(\"alpha\"), \"ok\")\n    this.assert.equal(this.findElement(\"div\").getAttribute(`data-${this.identifier}-alpha`), \"ok\")\n  }\n\n  \"test DataSet#has\"() {\n    this.assert.ok(this.controller.data.has(\"alpha\"))\n    this.assert.ok(this.controller.data.has(\"betaGamma\"))\n    this.assert.notOk(this.controller.data.has(\"nonexistent\"))\n  }\n\n  \"test DataSet#delete\"() {\n    this.controller.data.delete(\"alpha\")\n    this.assert.equal(this.controller.data.get(\"alpha\"), null)\n    this.assert.notOk(this.controller.data.has(\"alpha\"))\n    this.assert.notOk(this.findElement(\"div\").hasAttribute(`data-${this.identifier}-alpha`))\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/default_value_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { DefaultValueController } from \"../../controllers/default_value_controller\"\n\nexport default class DefaultValueTests extends ControllerTestCase(DefaultValueController) {\n  fixtureHTML = `\n    <div data-controller=\"${this.identifier}\"\n      data-${this.identifier}-default-string-override-value=\"I am the expected value\"\n      data-${this.identifier}-default-boolean-override-value=\"false\"\n      data-${this.identifier}-default-number-override-value=\"42\"\n      data-${this.identifier}-default-array-override-value=\"[9,8,7]\"\n      data-${this.identifier}-default-object-override-value='{\"expected\":\"value\"}'\n    </div>\n  `\n\n  // Booleans\n\n  \"test custom default boolean values\"() {\n    this.assert.deepEqual(this.controller.defaultBooleanValue, false)\n    this.assert.ok(this.controller.hasDefaultBooleanValue)\n    this.assert.deepEqual(this.get(\"default-boolean-value\"), null)\n\n    this.assert.deepEqual(this.controller.defaultBooleanTrueValue, true)\n    this.assert.ok(this.controller.hasDefaultBooleanTrueValue)\n    this.assert.deepEqual(this.get(\"default-boolean-true-value\"), null)\n\n    this.assert.deepEqual(this.controller.defaultBooleanFalseValue, false)\n    this.assert.ok(this.controller.hasDefaultBooleanFalseValue)\n    this.assert.deepEqual(this.get(\"default-boolean-false-value\"), null)\n  }\n\n  \"test should be able to set a new value for custom default boolean values\"() {\n    this.assert.deepEqual(this.get(\"default-boolean-true-value\"), null)\n    this.assert.deepEqual(this.controller.defaultBooleanTrueValue, true)\n    this.assert.ok(this.controller.hasDefaultBooleanTrueValue)\n\n    this.controller.defaultBooleanTrueValue = false\n\n    this.assert.deepEqual(this.get(\"default-boolean-true-value\"), \"false\")\n    this.assert.deepEqual(this.controller.defaultBooleanTrueValue, false)\n    this.assert.ok(this.controller.hasDefaultBooleanTrueValue)\n  }\n\n  \"test should override custom default boolean value with given data-attribute\"() {\n    this.assert.deepEqual(this.get(\"default-boolean-override-value\"), \"false\")\n    this.assert.deepEqual(this.controller.defaultBooleanOverrideValue, false)\n    this.assert.ok(this.controller.hasDefaultBooleanOverrideValue)\n  }\n\n  // Strings\n\n  \"test custom default string values\"() {\n    this.assert.deepEqual(this.controller.defaultStringValue, \"\")\n    this.assert.ok(this.controller.hasDefaultStringValue)\n    this.assert.deepEqual(this.get(\"default-string-value\"), null)\n\n    this.assert.deepEqual(this.controller.defaultStringHelloValue, \"Hello\")\n    this.assert.ok(this.controller.hasDefaultStringHelloValue)\n    this.assert.deepEqual(this.get(\"default-string-hello-value\"), null)\n  }\n\n  \"test should be able to set a new value for custom default string values\"() {\n    this.assert.deepEqual(this.get(\"default-string-value\"), null)\n    this.assert.deepEqual(this.controller.defaultStringValue, \"\")\n    this.assert.ok(this.controller.hasDefaultStringValue)\n\n    this.controller.defaultStringValue = \"New Value\"\n\n    this.assert.deepEqual(this.get(\"default-string-value\"), \"New Value\")\n    this.assert.deepEqual(this.controller.defaultStringValue, \"New Value\")\n    this.assert.ok(this.controller.hasDefaultStringValue)\n  }\n\n  \"test should override custom default string value with given data-attribute\"() {\n    this.assert.deepEqual(this.get(\"default-string-override-value\"), \"I am the expected value\")\n    this.assert.deepEqual(this.controller.defaultStringOverrideValue, \"I am the expected value\")\n    this.assert.ok(this.controller.hasDefaultStringOverrideValue)\n  }\n\n  // Numbers\n\n  \"test custom default number values\"() {\n    this.assert.deepEqual(this.controller.defaultNumberValue, 0)\n    this.assert.ok(this.controller.hasDefaultNumberValue)\n    this.assert.deepEqual(this.get(\"default-number-value\"), null)\n\n    this.assert.deepEqual(this.controller.defaultNumberThousandValue, 1000)\n    this.assert.ok(this.controller.hasDefaultNumberThousandValue)\n    this.assert.deepEqual(this.get(\"default-number-thousand-value\"), null)\n\n    this.assert.deepEqual(this.controller.defaultNumberZeroValue, 0)\n    this.assert.ok(this.controller.hasDefaultNumberZeroValue)\n    this.assert.deepEqual(this.get(\"default-number-zero-value\"), null)\n  }\n\n  \"test should be able to set a new value for custom default number values\"() {\n    this.assert.deepEqual(this.get(\"default-number-value\"), null)\n    this.assert.deepEqual(this.controller.defaultNumberValue, 0)\n    this.assert.ok(this.controller.hasDefaultNumberValue)\n\n    this.controller.defaultNumberValue = 123\n\n    this.assert.deepEqual(this.get(\"default-number-value\"), \"123\")\n    this.assert.deepEqual(this.controller.defaultNumberValue, 123)\n    this.assert.ok(this.controller.hasDefaultNumberValue)\n  }\n\n  \"test should override custom default number value with given data-attribute\"() {\n    this.assert.deepEqual(this.get(\"default-number-override-value\"), \"42\")\n    this.assert.deepEqual(this.controller.defaultNumberOverrideValue, 42)\n    this.assert.ok(this.controller.hasDefaultNumberOverrideValue)\n  }\n\n  // Arrays\n\n  \"test custom default array values\"() {\n    this.assert.deepEqual(this.controller.defaultArrayValue, [])\n    this.assert.ok(this.controller.hasDefaultArrayValue)\n    this.assert.deepEqual(this.get(\"default-array-value\"), null)\n\n    this.assert.deepEqual(this.controller.defaultArrayFilledValue, [1, 2, 3])\n    this.assert.ok(this.controller.hasDefaultArrayFilledValue)\n    this.assert.deepEqual(this.get(\"default-array-filled-value\"), null)\n  }\n\n  \"test should be able to set a new value for custom default array values\"() {\n    this.assert.deepEqual(this.get(\"default-array-value\"), null)\n    this.assert.deepEqual(this.controller.defaultArrayValue, [])\n    this.assert.ok(this.controller.hasDefaultArrayValue)\n\n    this.controller.defaultArrayValue = [1, 2]\n\n    this.assert.deepEqual(this.get(\"default-array-value\"), \"[1,2]\")\n    this.assert.deepEqual(this.controller.defaultArrayValue, [1, 2])\n    this.assert.ok(this.controller.hasDefaultArrayValue)\n  }\n\n  \"test should override custom default array value with given data-attribute\"() {\n    this.assert.deepEqual(this.get(\"default-array-override-value\"), \"[9,8,7]\")\n    this.assert.deepEqual(this.controller.defaultArrayOverrideValue, [9, 8, 7])\n    this.assert.ok(this.controller.hasDefaultArrayOverrideValue)\n  }\n\n  // Objects\n\n  \"test custom default object values\"() {\n    this.assert.deepEqual(this.controller.defaultObjectValue, {})\n    this.assert.ok(this.controller.hasDefaultObjectValue)\n    this.assert.deepEqual(this.get(\"default-object-value\"), null)\n\n    this.assert.deepEqual(this.controller.defaultObjectPersonValue, { name: \"David\" })\n    this.assert.ok(this.controller.hasDefaultObjectPersonValue)\n    this.assert.deepEqual(this.get(\"default-object-filled-value\"), null)\n  }\n\n  \"test should be able to set a new value for custom default object values\"() {\n    this.assert.deepEqual(this.get(\"default-object-value\"), null)\n    this.assert.deepEqual(this.controller.defaultObjectValue, {})\n    this.assert.ok(this.controller.hasDefaultObjectValue)\n\n    this.controller.defaultObjectValue = { new: \"value\" }\n\n    this.assert.deepEqual(this.get(\"default-object-value\"), '{\"new\":\"value\"}')\n    this.assert.deepEqual(this.controller.defaultObjectValue, { new: \"value\" })\n    this.assert.ok(this.controller.hasDefaultObjectValue)\n  }\n\n  \"test should override custom default object value with given data-attribute\"() {\n    this.assert.deepEqual(this.get(\"default-object-override-value\"), '{\"expected\":\"value\"}')\n    this.assert.deepEqual(this.controller.defaultObjectOverrideValue, { expected: \"value\" })\n    this.assert.ok(this.controller.hasDefaultObjectOverrideValue)\n  }\n\n  \"test [name]ValueChanged callbacks fire after initialize and before connect\"() {\n    this.assert.deepEqual(this.controller.lifecycleCallbacks, [\"initialize\", \"defaultBooleanValueChanged\", \"connect\"])\n  }\n\n  has(name: string) {\n    return this.element.hasAttribute(this.attr(name))\n  }\n\n  get(name: string) {\n    return this.element.getAttribute(this.attr(name))\n  }\n\n  set(name: string, value: string) {\n    return this.element.setAttribute(this.attr(name), value)\n  }\n\n  attr(name: string) {\n    return `data-${this.identifier}-${name}`\n  }\n\n  get element() {\n    return this.controller.element\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/error_handler_tests.ts",
    "content": "import { Controller } from \"../../../core/controller\"\nimport { Application } from \"../../../core/application\"\nimport { ControllerTestCase } from \"../../cases/controller_test_case\"\n\nclass MockLogger {\n  errors: any[] = []\n  logs: any[] = []\n  warns: any[] = []\n\n  log(event: any) {\n    this.logs.push(event)\n  }\n\n  error(event: any) {\n    this.errors.push(event)\n  }\n\n  warn(event: any) {\n    this.warns.push(event)\n  }\n\n  groupCollapsed() {}\n  groupEnd() {}\n}\n\nclass ErrorWhileConnectingController extends Controller {\n  connect() {\n    throw new Error(\"bad!\")\n  }\n}\n\nclass TestApplicationWithDefaultErrorBehavior extends Application {}\n\nexport default class ErrorHandlerTests extends ControllerTestCase(ErrorWhileConnectingController) {\n  controllerConstructor = ErrorWhileConnectingController\n\n  async setupApplication() {\n    const logger = new MockLogger()\n\n    this.application = new TestApplicationWithDefaultErrorBehavior(this.fixtureElement, this.schema)\n    this.application.logger = logger\n\n    window.onerror = function (message, source, lineno, colno, _error) {\n      logger.log(\n        `error from window.onerror. message = ${message}, source = ${source}, lineno = ${lineno}, colno = ${colno}`\n      )\n    }\n\n    super.setupApplication()\n  }\n\n  async \"test errors in connect are thrown and handled by built in logger\"() {\n    const mockLogger: any = this.application.logger\n\n    // when `ErrorWhileConnectingController#connect` throws, the controller's application's logger's `error` function\n    // is called; in this case that's `MockLogger#error`.\n    this.assert.equal(1, mockLogger.errors.length)\n  }\n\n  async \"test errors in connect are thrown and handled by window.onerror\"() {\n    const mockLogger: any = this.application.logger\n\n    this.assert.equal(1, mockLogger.logs.length)\n    this.assert.equal(\n      \"error from window.onerror. message = Error connecting controller, source = , lineno = 0, colno = 0\",\n      mockLogger.logs[0]\n    )\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/es6_tests.ts",
    "content": "import { LogController } from \"../../controllers/log_controller\"\nimport { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ES6Tests extends LogControllerTestCase {\n  static shouldSkipTest(_testName: string) {\n    return !(supportsES6Classes() && supportsReflectConstruct())\n  }\n\n  fixtureHTML = `\n    <div data-controller=\"es6\">\n      <button data-action=\"es6#log\">Log</button>\n    </div>\n  `\n\n  fixtureScript = `\n    _stimulus.application.register(\"es6\", class extends _stimulus.LogController {})\n  `\n\n  async renderFixture() {\n    ;(window as any)[\"_stimulus\"] = { LogController, application: this.application }\n    await super.renderFixture()\n\n    const scriptElement = document.createElement(\"script\")\n    scriptElement.textContent = this.fixtureScript\n    this.fixtureElement.appendChild(scriptElement)\n    await this.nextFrame\n  }\n\n  async teardown() {\n    this.application.unload(\"test\")\n    delete (window as any)[\"_stimulus\"]\n  }\n\n  async \"test ES6 controller classes\"() {\n    await this.triggerEvent(\"button\", \"click\")\n    this.assertActions({ eventType: \"click\", currentTarget: this.findElement(\"button\") })\n  }\n}\n\nfunction supportsES6Classes() {\n  try {\n    return eval(\"(class {}), true\")\n  } catch (error) {\n    return false\n  }\n}\n\nfunction supportsReflectConstruct() {\n  return typeof Reflect == \"object\" && typeof Reflect.construct == \"function\"\n}\n"
  },
  {
    "path": "src/tests/modules/core/event_options_tests.ts",
    "content": "import type { Controller } from \"src/core\"\nimport { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class EventOptionsTests extends LogControllerTestCase {\n  identifier = [\"c\", \"d\"]\n  fixtureHTML = `\n    <div data-controller=\"c d\">\n      <button></button>\n      <details></details>\n    </div>\n    <div id=\"outside\"></div>\n  `\n  async \"test different syntaxes for once action\"() {\n    await this.setAction(this.buttonElement, \"click->c#log:once d#log2:once c#log3:once\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log3\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement }\n    )\n  }\n\n  async \"test mix once and standard actions\"() {\n    await this.setAction(this.buttonElement, \"c#log:once d#log2 c#log3\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log3\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log2\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log3\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement }\n    )\n  }\n\n  async \"test stop propagation with once\"() {\n    await this.setAction(this.buttonElement, \"c#stop:once c#log\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"stop\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement })\n\n    await this.nextFrame\n    await this.triggerEvent(this.buttonElement, \"click\")\n    this.assertActions(\n      { name: \"stop\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement },\n      { name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement }\n    )\n  }\n\n  async \"test global once actions\"() {\n    await this.setAction(this.buttonElement, \"keydown@window->c#log:once\")\n\n    await this.triggerEvent(\"#outside\", \"keydown\")\n    await this.triggerEvent(\"#outside\", \"keydown\")\n\n    this.assertActions({ name: \"log\", eventType: \"keydown\" })\n  }\n\n  async \"test edge case when updating action list with setAttribute preserves once history\"() {\n    await this.setAction(this.buttonElement, \"c#log:once\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    //modify with a setAttribute and c#log should not be called anyhow\n    await this.setAction(this.buttonElement, \"c#log2 c#log:once d#log\")\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions(\n      { name: \"log\", identifier: \"c\" },\n      { name: \"log2\", identifier: \"c\" },\n      { name: \"log\", identifier: \"d\" }\n    )\n  }\n\n  async \"test default passive action\"() {\n    await this.setAction(this.buttonElement, \"scroll->c#logPassive:passive\")\n\n    await this.triggerEvent(this.buttonElement, \"scroll\", { setDefaultPrevented: false })\n    this.assertActions({ name: \"logPassive\", eventType: \"scroll\", passive: true })\n  }\n\n  async \"test global passive actions\"() {\n    await this.setAction(this.buttonElement, \"mouseup@window->c#logPassive:passive\")\n\n    await this.triggerEvent(\"#outside\", \"mouseup\", { setDefaultPrevented: false })\n    this.assertActions({ name: \"logPassive\", eventType: \"mouseup\", passive: true })\n  }\n\n  async \"test passive false actions\"() {\n    // by default touchmove is true in chrome\n    await this.setAction(this.buttonElement, \"touchmove@window->c#logPassive:!passive\")\n\n    await this.triggerEvent(\"#outside\", \"touchmove\", { setDefaultPrevented: false })\n    this.assertActions({ name: \"logPassive\", eventType: \"touchmove\", passive: false })\n  }\n\n  async \"test multiple options\"() {\n    // by default touchmove is true in chrome\n    await this.setAction(this.buttonElement, \"touchmove@window->c#logPassive:once:!passive\")\n\n    await this.triggerEvent(\"#outside\", \"touchmove\", { setDefaultPrevented: false })\n    await this.triggerEvent(\"#outside\", \"touchmove\", { setDefaultPrevented: false })\n    this.assertActions({ name: \"logPassive\", eventType: \"touchmove\", passive: false })\n  }\n\n  async \"test wrong options are silently ignored\"() {\n    await this.setAction(this.buttonElement, \"c#log:wrong:verywrong\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log\", identifier: \"c\" }, { name: \"log\", identifier: \"c\" })\n  }\n\n  async \"test stop option with implicit event\"() {\n    await this.setAction(this.element, \"click->c#log\")\n    await this.setAction(this.buttonElement, \"c#log2:stop\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log2\", eventType: \"click\" })\n  }\n\n  async \"test stop option with explicit event\"() {\n    await this.setAction(this.element, \"keydown->c#log\")\n    await this.setAction(this.buttonElement, \"keydown->c#log2:stop\")\n\n    await this.triggerEvent(this.buttonElement, \"keydown\")\n\n    this.assertActions({ name: \"log2\", eventType: \"keydown\" })\n  }\n\n  async \"test event propagation without stop option\"() {\n    await this.setAction(this.element, \"click->c#log\")\n    await this.setAction(this.buttonElement, \"c#log2\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log2\", eventType: \"click\" }, { name: \"log\", eventType: \"click\" })\n  }\n\n  async \"test prevent option with implicit event\"() {\n    await this.setAction(this.buttonElement, \"c#log:prevent\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log\", eventType: \"click\", defaultPrevented: true })\n  }\n\n  async \"test prevent option with explicit event\"() {\n    await this.setAction(this.buttonElement, \"keyup->c#log:prevent\")\n\n    await this.triggerEvent(this.buttonElement, \"keyup\")\n\n    this.assertActions({ name: \"log\", eventType: \"keyup\", defaultPrevented: true })\n  }\n\n  async \"test self option\"() {\n    await this.setAction(this.buttonElement, \"click->c#log:self\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log\", eventType: \"click\" })\n  }\n\n  async \"test self option on parent\"() {\n    await this.setAction(this.element, \"click->c#log:self\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertNoActions()\n  }\n\n  async \"test custom action option callback params contain the controller instance\"() {\n    let lastActionOptions: { controller?: Controller<Element> } = {}\n\n    const mockCallback = (options: Object) => {\n      lastActionOptions = options\n    }\n\n    this.application.registerActionOption(\"all\", (options: Object) => {\n      mockCallback(options)\n      return true\n    })\n\n    await this.setAction(this.buttonElement, \"click->c#log:all\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement })\n\n    this.assert.deepEqual([\"name\", \"value\", \"event\", \"element\", \"controller\"], Object.keys(lastActionOptions))\n\n    this.assert.equal(\n      lastActionOptions.controller,\n      this.application.getControllerForElementAndIdentifier(this.element, \"c\")\n    )\n\n    this.controllerConstructor.actionLog = [] // clear actions\n\n    await this.setAction(this.buttonElement, \"click->d#log:all\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log\", identifier: \"d\", eventType: \"click\", currentTarget: this.buttonElement })\n\n    this.assert.deepEqual([\"name\", \"value\", \"event\", \"element\", \"controller\"], Object.keys(lastActionOptions))\n\n    this.assert.equal(\n      lastActionOptions.controller,\n      this.application.getControllerForElementAndIdentifier(this.element, \"d\")\n    )\n  }\n\n  async \"test custom option\"() {\n    this.application.registerActionOption(\"open\", ({ value, event: { type, target } }) => {\n      switch (type) {\n        case \"toggle\":\n          return target instanceof HTMLDetailsElement && target.open == value\n        default:\n          return true\n      }\n    })\n    await this.setAction(this.detailsElement, \"toggle->c#log:open\")\n\n    await this.toggleElement(this.detailsElement)\n    await this.toggleElement(this.detailsElement)\n    await this.toggleElement(this.detailsElement)\n\n    this.assertActions({ name: \"log\", eventType: \"toggle\" }, { name: \"log\", eventType: \"toggle\" })\n  }\n\n  async \"test inverted custom option\"() {\n    this.application.registerActionOption(\"open\", ({ value, event: { type, target } }) => {\n      switch (type) {\n        case \"toggle\":\n          return target instanceof HTMLDetailsElement && target.open == value\n        default:\n          return true\n      }\n    })\n    await this.setAction(this.detailsElement, \"toggle->c#log:!open\")\n\n    await this.toggleElement(this.detailsElement)\n    await this.toggleElement(this.detailsElement)\n    await this.toggleElement(this.detailsElement)\n\n    this.assertActions({ name: \"log\", eventType: \"toggle\" })\n  }\n\n  async \"test custom action option callback event contains params\"() {\n    let lastActionEventParams: Object = {}\n\n    // clone the params to ensure we check the value as the callback receives it\n    // not the event after all actions have resolved\n\n    const mockCallback = ({ event: { params = {} } = {} }) => {\n      lastActionEventParams = { ...params }\n    }\n\n    this.application.registerActionOption(\"all\", (options: Object) => {\n      mockCallback(options)\n      return true\n    })\n\n    this.buttonElement.setAttribute(\"data-c-custom-number-param\", \"41\")\n    this.buttonElement.setAttribute(\"data-c-custom-string-param\", \"validation\")\n    this.buttonElement.setAttribute(\"data-c-custom-boolean-param\", \"true\")\n    this.buttonElement.setAttribute(\"data-d-should-ignore-param\", \"_IGNORED_\")\n\n    await this.setAction(this.buttonElement, \"click->c#log:all\")\n\n    await this.triggerEvent(this.buttonElement, \"click\")\n\n    this.assertActions({ name: \"log\", identifier: \"c\", eventType: \"click\", currentTarget: this.buttonElement })\n\n    const expectedEventParams = {\n      customBoolean: true,\n      customNumber: 41,\n      customString: \"validation\",\n    }\n\n    this.assert.deepEqual(this.controllerConstructor.actionLog[0].params, expectedEventParams)\n\n    this.assert.deepEqual(lastActionEventParams, expectedEventParams)\n  }\n\n  setAction(element: Element, value: string) {\n    element.setAttribute(\"data-action\", value)\n    return this.nextFrame\n  }\n\n  toggleElement(details: Element) {\n    details.toggleAttribute(\"open\")\n    return this.nextFrame\n  }\n\n  get element() {\n    return this.findElement(\"div\")\n  }\n\n  get buttonElement() {\n    return this.findElement(\"button\")\n  }\n\n  get detailsElement() {\n    return this.findElement(\"details\")\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/extending_application_tests.ts",
    "content": "import { Application } from \"../../../core/application\"\nimport { DOMTestCase } from \"../../cases/dom_test_case\"\nimport { ActionDescriptorFilter } from \"src/core/action_descriptor\"\n\nconst mockCallback: { (label: string): void; lastCall: string | null } = (label: string) => {\n  mockCallback.lastCall = label\n}\nmockCallback.lastCall = null\n\nclass TestApplicationWithCustomBehavior extends Application {\n  registerActionOption(name: string, filter: ActionDescriptorFilter): void {\n    mockCallback(`registerActionOption:${name}`)\n    super.registerActionOption(name, filter)\n  }\n}\n\nexport default class ExtendingApplicationTests extends DOMTestCase {\n  application!: Application\n\n  async runTest(testName: string) {\n    try {\n      // use the documented way to start & reference only the returned application instance\n      this.application = TestApplicationWithCustomBehavior.start(this.fixtureElement)\n      await super.runTest(testName)\n    } finally {\n      this.application.stop()\n    }\n  }\n\n  async setup() {\n    mockCallback.lastCall = null\n  }\n\n  async teardown() {\n    mockCallback.lastCall = null\n  }\n\n  async \"test extended class method is supported when using MyApplication.start()\"() {\n    this.assert.equal(mockCallback.lastCall, null)\n\n    const mockTrue = () => true\n    this.application.registerActionOption(\"kbd\", mockTrue)\n    this.assert.equal(this.application.actionDescriptorFilters[\"kbd\"], mockTrue)\n    this.assert.equal(mockCallback.lastCall, \"registerActionOption:kbd\")\n\n    const mockFalse = () => false\n    this.application.registerActionOption(\"xyz\", mockFalse)\n    this.assert.equal(this.application.actionDescriptorFilters[\"xyz\"], mockFalse)\n    this.assert.equal(mockCallback.lastCall, \"registerActionOption:xyz\")\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/legacy_target_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { TargetController } from \"../../controllers/target_controller\"\n\nexport default class LegacyTargetTests extends ControllerTestCase(TargetController) {\n  fixtureHTML = `\n    <div data-controller=\"${this.identifier}\">\n      <div data-target=\"${this.identifier}.alpha\" id=\"alpha1\"></div>\n      <div data-target=\"${this.identifier}.alpha\" id=\"alpha2\"></div>\n      <div data-target=\"${this.identifier}.beta\" data-${this.identifier}-target=\"gamma\" id=\"beta1\">\n        <div data-target=\"${this.identifier}.gamma\" id=\"gamma1\"></div>\n      </div>\n      <div data-controller=\"${this.identifier}\" id=\"child\">\n        <div data-target=\"${this.identifier}.delta\" id=\"delta1\"></div>\n      </div>\n      <textarea data-target=\"${this.identifier}.input\" id=\"input1\"></textarea>\n    </div>\n  `\n\n  warningCount = 0\n\n  async setupApplication() {\n    super.setupApplication()\n    this.application.logger = Object.create(console, {\n      warn: {\n        value: () => this.warningCount++,\n      },\n    })\n  }\n\n  \"test TargetSet#find\"() {\n    this.assert.equal(this.controller.targets.find(\"alpha\"), this.findElement(\"#alpha1\"))\n    this.assert.equal(this.warningCount, 1)\n  }\n\n  \"test TargetSet#find prefers scoped target attributes\"() {\n    this.assert.equal(this.controller.targets.find(\"gamma\"), this.findElement(\"#beta1\"))\n    this.assert.equal(this.warningCount, 0)\n  }\n\n  \"test TargetSet#findAll\"() {\n    this.assert.deepEqual(this.controller.targets.findAll(\"alpha\"), this.findElements(\"#alpha1\", \"#alpha2\"))\n    this.assert.equal(this.warningCount, 2)\n  }\n\n  \"test TargetSet#findAll prioritizes scoped target attributes\"() {\n    this.assert.deepEqual(this.controller.targets.findAll(\"gamma\"), this.findElements(\"#beta1\", \"#gamma1\"))\n    this.assert.equal(this.warningCount, 1)\n  }\n\n  \"test TargetSet#findAll with multiple arguments\"() {\n    this.assert.deepEqual(\n      this.controller.targets.findAll(\"alpha\", \"beta\"),\n      this.findElements(\"#alpha1\", \"#alpha2\", \"#beta1\")\n    )\n    this.assert.equal(this.warningCount, 3)\n  }\n\n  \"test TargetSet#has\"() {\n    this.assert.equal(this.controller.targets.has(\"gamma\"), true)\n    this.assert.equal(this.controller.targets.has(\"delta\"), false)\n    this.assert.equal(this.warningCount, 0)\n  }\n\n  \"test TargetSet#find ignores child controller targets\"() {\n    this.assert.equal(this.controller.targets.find(\"delta\"), null)\n    this.findElement(\"#child\").removeAttribute(\"data-controller\")\n    this.assert.equal(this.controller.targets.find(\"delta\"), this.findElement(\"#delta1\"))\n    this.assert.equal(this.warningCount, 1)\n  }\n\n  \"test linked target properties\"() {\n    this.assert.equal(this.controller.betaTarget, this.findElement(\"#beta1\"))\n    this.assert.deepEqual(this.controller.betaTargets, this.findElements(\"#beta1\"))\n    this.assert.equal(this.controller.hasBetaTarget, true)\n    this.assert.equal(this.warningCount, 1)\n  }\n\n  \"test inherited linked target properties\"() {\n    this.assert.equal(this.controller.alphaTarget, this.findElement(\"#alpha1\"))\n    this.assert.deepEqual(this.controller.alphaTargets, this.findElements(\"#alpha1\", \"#alpha2\"))\n    this.assert.equal(this.warningCount, 2)\n  }\n\n  \"test singular linked target property throws an error when no target is found\"() {\n    this.findElement(\"#beta1\").removeAttribute(\"data-target\")\n    this.assert.equal(this.controller.hasBetaTarget, false)\n    this.assert.equal(this.controller.betaTargets.length, 0)\n    this.assert.throws(() => this.controller.betaTarget)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/lifecycle_tests.ts",
    "content": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class LifecycleTests extends LogControllerTestCase {\n  controllerElement!: Element\n\n  async setup() {\n    this.controllerElement = this.controller.element\n  }\n\n  async \"test Controller#initialize\"() {\n    const controller = this.controller\n    this.assert.equal(controller.initializeCount, 1)\n    await this.reconnectControllerElement()\n    this.assert.equal(this.controller, controller)\n    this.assert.equal(controller.initializeCount, 1)\n  }\n\n  async \"test Controller#connect\"() {\n    this.assert.equal(this.controller.connectCount, 1)\n    await this.reconnectControllerElement()\n    this.assert.equal(this.controller.connectCount, 2)\n  }\n\n  async \"test Controller#disconnect\"() {\n    const controller = this.controller\n    this.assert.equal(controller.disconnectCount, 0)\n    await this.disconnectControllerElement()\n    this.assert.equal(controller.disconnectCount, 1)\n  }\n\n  async reconnectControllerElement() {\n    await this.disconnectControllerElement()\n    await this.connectControllerElement()\n  }\n\n  async connectControllerElement() {\n    this.fixtureElement.appendChild(this.controllerElement)\n    await this.nextFrame\n  }\n\n  async disconnectControllerElement() {\n    this.fixtureElement.removeChild(this.controllerElement)\n    await this.nextFrame\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/loading_tests.ts",
    "content": "import { ApplicationTestCase } from \"../../cases/application_test_case\"\nimport { LogController } from \"../../controllers/log_controller\"\n\nclass UnloadableController extends LogController {\n  static get shouldLoad() {\n    return false\n  }\n}\nclass LoadableController extends LogController {\n  static get shouldLoad() {\n    return true\n  }\n}\n\nclass AfterLoadController extends LogController {\n  static values = {\n    example: { default: \"demo\", type: String },\n  }\n\n  static afterLoad(identifier: string, application: any) {\n    const newElement = document.createElement(\"div\")\n    newElement.classList.add(\"after-load-test\")\n    newElement.setAttribute(application.schema.controllerAttribute, identifier)\n    application.element.append(newElement)\n    document.dispatchEvent(\n      new CustomEvent(\"test\", {\n        detail: { identifier, application, exampleDefault: this.values.example.default, controller: this },\n      })\n    )\n  }\n}\n\nexport default class ApplicationTests extends ApplicationTestCase {\n  fixtureHTML = `<div data-controller=\"loadable\"><div data-controller=\"unloadable\">`\n\n  \"test module with false shouldLoad should not load when registering\"() {\n    this.application.register(\"unloadable\", UnloadableController)\n    this.assert.equal(this.controllers.length, 0)\n  }\n\n  \"test module with true shouldLoad should load when registering\"() {\n    this.application.register(\"loadable\", LoadableController)\n    this.assert.equal(this.controllers.length, 1)\n  }\n\n  \"test module with afterLoad method should be triggered when registered\"() {\n    // set up an event listener to track the params passed into the AfterLoadController\n    let data: { application?: any; identifier?: string; exampleDefault?: string; controller?: any } = {}\n    document.addEventListener(\"test\", (({ detail }: CustomEvent) => {\n      data = detail\n    }) as EventListener)\n\n    this.assert.equal(data.application, undefined)\n    this.assert.equal(data.controller, undefined)\n    this.assert.equal(data.exampleDefault, undefined)\n    this.assert.equal(data.identifier, undefined)\n\n    this.application.register(\"after-load\", AfterLoadController)\n\n    // check the DOM element has been added based on params provided\n    this.assert.equal(this.findElements('[data-controller=\"after-load\"]').length, 1)\n\n    // check that static method was correctly called with the params\n    this.assert.equal(data.application, this.application)\n    this.assert.equal(data.controller, AfterLoadController)\n    this.assert.equal(data.exampleDefault, \"demo\")\n    this.assert.equal(data.identifier, \"after-load\")\n  }\n\n  get controllers() {\n    return this.application.controllers as LogController[]\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/memory_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\n\nexport default class MemoryTests extends ControllerTestCase() {\n  controllerElement!: Element\n\n  async setup() {\n    this.controllerElement = this.controller.element\n  }\n\n  fixtureHTML = `\n    <div data-controller=\"${this.identifier}\">\n      <button data-action=\"${this.identifier}#doLog\">Log</button>\n      <button data-action=\"${this.identifier}#doAlert\">Alert</button>\n    </div>\n  `\n\n  async \"test removing a controller clears dangling eventListeners\"() {\n    this.assert.equal(this.application.dispatcher.eventListeners.length, 2)\n    await this.fixtureElement.removeChild(this.controllerElement)\n    this.assert.equal(this.application.dispatcher.eventListeners.length, 0)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/outlet_order_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { OutletController } from \"../../controllers/outlet_controller\"\n\nconst connectOrder: string[] = []\n\nclass OutletOrderController extends OutletController {\n  connect() {\n    connectOrder.push(`${this.identifier}-${this.element.id}-start`)\n    super.connect()\n    connectOrder.push(`${this.identifier}-${this.element.id}-end`)\n  }\n}\n\nexport default class OutletOrderTests extends ControllerTestCase(OutletOrderController) {\n  fixtureHTML = `\n    <div data-controller=\"alpha\" id=\"alpha1\" data-alpha-beta-outlet=\".beta\">Search</div>\n    <div data-controller=\"beta\" id=\"beta-1\" class=\"beta\">Beta</div>\n    <div data-controller=\"beta\" id=\"beta-2\" class=\"beta\">Beta</div>\n    <div data-controller=\"beta\" id=\"beta-3\" class=\"beta\">Beta</div>\n  `\n\n  get identifiers() {\n    return [\"alpha\", \"beta\"]\n  }\n\n  async \"test can access outlets in connect() even if they are referenced before they are connected\"() {\n    this.assert.equal(this.controller.betaOutletsInConnectValue, 3)\n\n    this.controller.betaOutlets.forEach((outlet) => {\n      this.assert.equal(outlet.identifier, \"beta\")\n      this.assert.equal(Array.from(outlet.element.classList.values()), \"beta\")\n    })\n\n    this.assert.deepEqual(connectOrder, [\n      \"alpha-alpha1-start\",\n      \"beta-beta-1-start\",\n      \"beta-beta-1-end\",\n      \"beta-beta-2-start\",\n      \"beta-beta-2-end\",\n      \"beta-beta-3-start\",\n      \"beta-beta-3-end\",\n      \"alpha-alpha1-end\",\n    ])\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/outlet_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { OutletController } from \"../../controllers/outlet_controller\"\n\nexport default class OutletTests extends ControllerTestCase(OutletController) {\n  fixtureHTML = `\n    <div id=\"container\">\n      <div data-controller=\"alpha\" class=\"alpha\" id=\"alpha1\"></div>\n      <div data-controller=\"alpha\" class=\"alpha\" id=\"alpha2\"></div>\n\n      <div data-controller=\"beta\" class=\"beta\" id=\"beta1\">\n        <div data-controller=\"beta\" class=\"beta\" id=\"beta2\"></div>\n        <div id=\"beta3\"></div>\n        <div data-controller=\"beta\" id=\"beta4\"></div>\n      </div>\n\n      <div\n        data-controller=\"${this.identifier}\"\n        data-${this.identifier}-connected-class=\"connected\"\n        data-${this.identifier}-disconnected-class=\"disconnected\"\n        data-${this.identifier}-alpha-outlet=\"#alpha1,#alpha2\"\n        data-${this.identifier}-beta-outlet=\".beta\"\n        data-${this.identifier}-delta-outlet=\".delta\"\n        data-${this.identifier}-namespaced--epsilon-outlet=\".epsilon\"\n      >\n        <div data-controller=\"gamma\" class=\"gamma\" id=\"gamma2\"></div>\n      </div>\n\n      <div data-controller=\"delta gamma\" class=\"delta gamma\" id=\"delta1\">\n        <div data-controller=\"gamma\" class=\"gamma\" id=\"gamma1\"></div>\n      </div>\n\n      <div data-controller=\"namespaced--epsilon\" class=\"epsilon\" id=\"epsilon1\"></div>\n\n      <div data-controller=\"namespaced--epsilon\" class=\"epsilon\" id=\"epsilon2\"></div>\n\n      <div class=\"beta\" id=\"beta5\"></div>\n    </div>\n  `\n  get identifiers() {\n    return [\"test\", \"alpha\", \"beta\", \"gamma\", \"delta\", \"omega\", \"namespaced--epsilon\"]\n  }\n\n  \"test OutletSet#find\"() {\n    this.assert.equal(this.controller.outlets.find(\"alpha\"), this.findElement(\"#alpha1\"))\n    this.assert.equal(this.controller.outlets.find(\"beta\"), this.findElement(\"#beta1\"))\n    this.assert.equal(this.controller.outlets.find(\"delta\"), this.findElement(\"#delta1\"))\n    this.assert.equal(this.controller.outlets.find(\"namespaced--epsilon\"), this.findElement(\"#epsilon1\"))\n  }\n\n  \"test OutletSet#findAll\"() {\n    this.assert.deepEqual(this.controller.outlets.findAll(\"alpha\"), this.findElements(\"#alpha1\", \"#alpha2\"))\n    this.assert.deepEqual(this.controller.outlets.findAll(\"beta\"), this.findElements(\"#beta1\", \"#beta2\"))\n    this.assert.deepEqual(\n      this.controller.outlets.findAll(\"namespaced--epsilon\"),\n      this.findElements(\"#epsilon1\", \"#epsilon2\")\n    )\n  }\n\n  \"test OutletSet#findAll with multiple arguments\"() {\n    this.assert.deepEqual(\n      this.controller.outlets.findAll(\"alpha\", \"beta\", \"namespaced--epsilon\"),\n      this.findElements(\"#alpha1\", \"#alpha2\", \"#beta1\", \"#beta2\", \"#epsilon1\", \"#epsilon2\")\n    )\n  }\n\n  \"test OutletSet#has\"() {\n    this.assert.equal(this.controller.outlets.has(\"alpha\"), true)\n    this.assert.equal(this.controller.outlets.has(\"beta\"), true)\n    this.assert.equal(this.controller.outlets.has(\"gamma\"), false)\n    this.assert.equal(this.controller.outlets.has(\"delta\"), true)\n    this.assert.equal(this.controller.outlets.has(\"omega\"), false)\n    this.assert.equal(this.controller.outlets.has(\"namespaced--epsilon\"), true)\n  }\n\n  \"test OutletSet#has when attribute gets added later\"() {\n    this.assert.equal(this.controller.outlets.has(\"gamma\"), false)\n    this.controller.element.setAttribute(`data-${this.identifier}-gamma-outlet`, \".gamma\")\n    this.assert.equal(this.controller.outlets.has(\"gamma\"), true)\n  }\n\n  \"test OutletSet#has when no element with selector exists\"() {\n    this.controller.element.setAttribute(`data-${this.identifier}-gamma-outlet`, \"#doesntexist\")\n    this.assert.equal(this.controller.outlets.has(\"gamma\"), false)\n  }\n\n  \"test OutletSet#has when selector matches but element doesn't have the right controller\"() {\n    this.controller.element.setAttribute(`data-${this.identifier}-gamma-outlet`, \".alpha\")\n    this.assert.equal(this.controller.outlets.has(\"gamma\"), false)\n  }\n\n  \"test linked outlet properties\"() {\n    const element = this.findElement(\"#beta1\")\n    const betaOutlet = this.controller.application.getControllerForElementAndIdentifier(element, \"beta\")\n    this.assert.equal(this.controller.betaOutlet, betaOutlet)\n    this.assert.equal(this.controller.betaOutletElement, element)\n\n    const elements = this.findElements(\"#beta1\", \"#beta2\")\n    const betaOutlets = elements.map((element) =>\n      this.controller.application.getControllerForElementAndIdentifier(element, \"beta\")\n    )\n    this.assert.deepEqual(this.controller.betaOutlets, betaOutlets)\n    this.assert.deepEqual(this.controller.betaOutletElements, elements)\n\n    this.assert.equal(this.controller.hasBetaOutlet, true)\n  }\n\n  \"test inherited linked outlet properties\"() {\n    const element = this.findElement(\"#alpha1\")\n    const alphaOutlet = this.controller.application.getControllerForElementAndIdentifier(element, \"alpha\")\n    this.assert.equal(this.controller.alphaOutlet, alphaOutlet)\n    this.assert.equal(this.controller.alphaOutletElement, element)\n\n    const elements = this.findElements(\"#alpha1\", \"#alpha2\")\n    const alphaOutlets = elements.map((element) =>\n      this.controller.application.getControllerForElementAndIdentifier(element, \"alpha\")\n    )\n    this.assert.deepEqual(this.controller.alphaOutlets, alphaOutlets)\n    this.assert.deepEqual(this.controller.alphaOutletElements, elements)\n  }\n\n  \"test singular linked outlet property throws an error when no outlet is found\"() {\n    this.findElements(\"#alpha1\", \"#alpha2\").forEach((e) => {\n      e.removeAttribute(\"id\")\n      e.removeAttribute(\"class\")\n      e.removeAttribute(\"data-controller\")\n    })\n\n    this.assert.equal(this.controller.hasAlphaOutlet, false)\n    this.assert.equal(this.controller.alphaOutlets.length, 0)\n    this.assert.equal(this.controller.alphaOutletElements.length, 0)\n    this.assert.throws(() => this.controller.alphaOutlet)\n    this.assert.throws(() => this.controller.alphaOutletElement)\n  }\n\n  async \"test outlet connected callback fires\"() {\n    const alphaOutlets = this.controller.alphaOutletElements.filter((outlet) => outlet.classList.contains(\"connected\"))\n\n    this.assert.equal(alphaOutlets.length, 2)\n    this.assert.equal(this.controller.alphaOutletConnectedCallCountValue, 2)\n  }\n\n  \"test outlet connected callback fires for namespaced outlets\"() {\n    const epsilonOutlets = this.controller.namespacedEpsilonOutletElements.filter((outlet) =>\n      outlet.classList.contains(\"connected\")\n    )\n    this.assert.equal(epsilonOutlets.length, 2)\n    this.assert.equal(this.controller.namespacedEpsilonOutletConnectedCallCountValue, 2)\n  }\n\n  async \"test outlet connected callback when element is inserted\"() {\n    const betaOutletElement = document.createElement(\"div\")\n    await this.setAttribute(betaOutletElement, \"class\", \"beta\")\n    await this.setAttribute(betaOutletElement, \"data-controller\", \"beta\")\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 2)\n\n    await this.appendChild(this.controller.element, betaOutletElement)\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 3)\n    this.assert.ok(\n      betaOutletElement.classList.contains(\"connected\"),\n      `expected \"${betaOutletElement.className}\" to contain \"connected\"`\n    )\n    this.assert.ok(betaOutletElement.isConnected, \"element is present in document\")\n\n    await this.appendChild(\"#container\", betaOutletElement.cloneNode(true))\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 4)\n  }\n\n  async \"test outlet connected callback when present element adds matching outlet selector attribute\"() {\n    const element = this.findElement(\"#beta3\")\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 2)\n\n    await this.setAttribute(element, \"data-controller\", \"beta\")\n    await this.setAttribute(element, \"class\", \"beta\")\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 3)\n    this.assert.ok(element.classList.contains(\"connected\"), `expected \"${element.className}\" to contain \"connected\"`)\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test outlet connected callback when present element already has connected controller and adds matching outlet selector attribute\"() {\n    const element = this.findElement(\"#beta4\")\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 2)\n\n    await this.setAttribute(element, \"class\", \"beta\")\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 3)\n    this.assert.ok(element.classList.contains(\"connected\"), `expected \"${element.className}\" to contain \"connected\"`)\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test outlet connect callback when an outlet present in the document adds a matching data-controller attribute\"() {\n    const element = this.findElement(\"#beta5\")\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 2)\n\n    await this.setAttribute(element, \"data-controller\", \"beta\")\n\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 3)\n    this.assert.ok(element.classList.contains(\"connected\"), `expected \"${element.className}\" to contain \"connected\"`)\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test outlet disconnected callback fires when calling disconnect() on the controller\"() {\n    this.assert.equal(\n      this.controller.alphaOutletElements.filter((outlet) => outlet.classList.contains(\"disconnected\")).length,\n      0\n    )\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0)\n\n    this.controller.context.disconnect()\n    await this.nextFrame\n\n    this.assert.equal(\n      this.controller.alphaOutletElements.filter((outlet) => outlet.classList.contains(\"disconnected\")).length,\n      2\n    )\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 2)\n  }\n\n  async \"test outlet disconnected callback when element is removed\"() {\n    const disconnectedAlpha = this.findElement(\"#alpha1\")\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0)\n    this.assert.notOk(\n      disconnectedAlpha.classList.contains(\"disconnected\"),\n      `expected \"${disconnectedAlpha.className}\" not to contain \"disconnected\"`\n    )\n\n    await this.remove(disconnectedAlpha)\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 1)\n    this.assert.ok(\n      disconnectedAlpha.classList.contains(\"disconnected\"),\n      `expected \"${disconnectedAlpha.className}\" to contain \"disconnected\"`\n    )\n    this.assert.notOk(disconnectedAlpha.isConnected, \"element is not present in document\")\n  }\n\n  async \"test outlet disconnected callback when element is removed with namespaced outlet\"() {\n    const disconnectedEpsilon = this.findElement(\"#epsilon1\")\n\n    this.assert.equal(this.controller.namespacedEpsilonOutletDisconnectedCallCountValue, 0)\n    this.assert.notOk(\n      disconnectedEpsilon.classList.contains(\"disconnected\"),\n      `expected \"${disconnectedEpsilon.className}\" not to contain \"disconnected\"`\n    )\n\n    await this.remove(disconnectedEpsilon)\n\n    this.assert.equal(this.controller.namespacedEpsilonOutletDisconnectedCallCountValue, 1)\n    this.assert.ok(\n      disconnectedEpsilon.classList.contains(\"disconnected\"),\n      `expected \"${disconnectedEpsilon.className}\" to contain \"disconnected\"`\n    )\n    this.assert.notOk(disconnectedEpsilon.isConnected, \"element is not present in document\")\n  }\n\n  async \"test outlet disconnected callback when an outlet present in the document removes the selector attribute\"() {\n    const element = this.findElement(\"#alpha1\")\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0)\n    this.assert.notOk(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" not to contain \"disconnected\"`\n    )\n\n    await this.removeAttribute(element, \"id\")\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 1)\n    this.assert.ok(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" to contain \"disconnected\"`\n    )\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test outlet disconnected callback when an outlet present in the document removes the data-controller attribute\"() {\n    const element = this.findElement(\"#alpha1\")\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0)\n    this.assert.notOk(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" not to contain \"disconnected\"`\n    )\n\n    await this.removeAttribute(element, \"data-controller\")\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 1)\n    this.assert.ok(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" to contain \"disconnected\"`\n    )\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test outlet connect callback when the controlled element's outlet attribute is added\"() {\n    const gamma2 = this.findElement(\"#gamma2\")\n\n    await this.setAttribute(this.controller.element, `data-${this.identifier}-gamma-outlet`, \"#gamma2\")\n\n    this.assert.equal(this.controller.gammaOutletConnectedCallCountValue, 1)\n    this.assert.ok(gamma2.isConnected, \"#gamma2 is still present in document\")\n    this.assert.ok(gamma2.classList.contains(\"connected\"), `expected \"${gamma2.className}\" to contain \"connected\"`)\n  }\n\n  async \"test outlet connect callback doesn't get trigged when any attribute gets added to the controller element\"() {\n    this.assert.equal(this.controller.alphaOutletConnectedCallCountValue, 2)\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 2)\n    this.assert.equal(this.controller.gammaOutletConnectedCallCountValue, 0)\n    this.assert.equal(this.controller.namespacedEpsilonOutletConnectedCallCountValue, 2)\n\n    await this.setAttribute(this.controller.element, \"data-some-random-attribute\", \"#alpha1\")\n\n    this.assert.equal(this.controller.alphaOutletConnectedCallCountValue, 2)\n    this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 2)\n    this.assert.equal(this.controller.gammaOutletConnectedCallCountValue, 0)\n    this.assert.equal(this.controller.namespacedEpsilonOutletConnectedCallCountValue, 2)\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0)\n    this.assert.equal(this.controller.betaOutletDisconnectedCallCountValue, 0)\n    this.assert.equal(this.controller.gammaOutletDisconnectedCallCountValue, 0)\n    this.assert.equal(this.controller.namespacedEpsilonOutletDisconnectedCallCountValue, 0)\n  }\n\n  async \"test outlet connect callback when the controlled element's outlet attribute is changed\"() {\n    const alpha1 = this.findElement(\"#alpha1\")\n    const alpha2 = this.findElement(\"#alpha2\")\n\n    await this.setAttribute(this.controller.element, `data-${this.identifier}-alpha-outlet`, \"#alpha1\")\n\n    this.assert.equal(this.controller.alphaOutletConnectedCallCountValue, 2)\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 1)\n    this.assert.ok(alpha1.isConnected, \"alpha1 is still present in document\")\n    this.assert.ok(alpha2.isConnected, \"alpha2 is still present in document\")\n    this.assert.ok(alpha1.classList.contains(\"connected\"), `expected \"${alpha1.className}\" to contain \"connected\"`)\n    this.assert.notOk(\n      alpha1.classList.contains(\"disconnected\"),\n      `expected \"${alpha1.className}\" to contain \"disconnected\"`\n    )\n    this.assert.ok(\n      alpha2.classList.contains(\"disconnected\"),\n      `expected \"${alpha2.className}\" to contain \"disconnected\"`\n    )\n  }\n\n  async \"test outlet disconnected callback when the controlled element's outlet attribute is removed\"() {\n    const alpha1 = this.findElement(\"#alpha1\")\n    const alpha2 = this.findElement(\"#alpha2\")\n\n    await this.removeAttribute(this.controller.element, `data-${this.identifier}-alpha-outlet`)\n\n    this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 2)\n    this.assert.ok(alpha1.isConnected, \"#alpha1 is still present in document\")\n    this.assert.ok(alpha2.isConnected, \"#alpha2 is still present in document\")\n    this.assert.ok(\n      alpha1.classList.contains(\"disconnected\"),\n      `expected \"${alpha1.className}\" to contain \"disconnected\"`\n    )\n    this.assert.ok(\n      alpha2.classList.contains(\"disconnected\"),\n      `expected \"${alpha2.className}\" to contain \"disconnected\"`\n    )\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/string_helpers_tests.ts",
    "content": "import { TestCase } from \"../../cases/test_case\"\nimport * as helpers from \"../../../core/string_helpers\"\n\nexport default class StringHelpersTests extends TestCase {\n  \"test should camelize strings\"() {\n    this.assert.equal(helpers.camelize(\"underscore_value\"), \"underscoreValue\")\n    this.assert.equal(helpers.camelize(\"Underscore_value\"), \"UnderscoreValue\")\n    this.assert.equal(helpers.camelize(\"underscore_Value\"), \"underscore_Value\")\n    this.assert.equal(helpers.camelize(\"Underscore_Value\"), \"Underscore_Value\")\n    this.assert.equal(helpers.camelize(\"multi_underscore_value\"), \"multiUnderscoreValue\")\n\n    this.assert.equal(helpers.camelize(\"dash-value\"), \"dashValue\")\n    this.assert.equal(helpers.camelize(\"Dash-value\"), \"DashValue\")\n    this.assert.equal(helpers.camelize(\"dash-Value\"), \"dash-Value\")\n    this.assert.equal(helpers.camelize(\"Dash-Value\"), \"Dash-Value\")\n    this.assert.equal(helpers.camelize(\"multi-dash-value\"), \"multiDashValue\")\n  }\n\n  \"test should namespace camelize strings\"() {\n    this.assert.equal(helpers.namespaceCamelize(\"underscore__value\"), \"underscoreValue\")\n    this.assert.equal(helpers.namespaceCamelize(\"Underscore__value\"), \"UnderscoreValue\")\n    this.assert.equal(helpers.namespaceCamelize(\"underscore__Value\"), \"underscore_Value\")\n    this.assert.equal(helpers.namespaceCamelize(\"Underscore__Value\"), \"Underscore_Value\")\n    this.assert.equal(helpers.namespaceCamelize(\"multi__underscore__value\"), \"multiUnderscoreValue\")\n\n    this.assert.equal(helpers.namespaceCamelize(\"dash--value\"), \"dashValue\")\n    this.assert.equal(helpers.namespaceCamelize(\"Dash--value\"), \"DashValue\")\n    this.assert.equal(helpers.namespaceCamelize(\"dash--Value\"), \"dash-Value\")\n    this.assert.equal(helpers.namespaceCamelize(\"Dash--Value\"), \"Dash-Value\")\n    this.assert.equal(helpers.namespaceCamelize(\"multi--dash--value\"), \"multiDashValue\")\n  }\n\n  \"test should dasherize strings\"() {\n    this.assert.equal(helpers.dasherize(\"camelizedValue\"), \"camelized-value\")\n    this.assert.equal(helpers.dasherize(\"longCamelizedValue\"), \"long-camelized-value\")\n  }\n\n  \"test should capitalize strings\"() {\n    this.assert.equal(helpers.capitalize(\"lowercase\"), \"Lowercase\")\n    this.assert.equal(helpers.capitalize(\"Uppercase\"), \"Uppercase\")\n  }\n\n  \"test should tokenize strings\"() {\n    this.assert.deepEqual(helpers.tokenize(\"\"), [])\n    this.assert.deepEqual(helpers.tokenize(\"one\"), [\"one\"])\n    this.assert.deepEqual(helpers.tokenize(\"two words\"), [\"two\", \"words\"])\n    this.assert.deepEqual(helpers.tokenize(\"a_lot of-words with special--chars mixed__in\"), [\n      \"a_lot\",\n      \"of-words\",\n      \"with\",\n      \"special--chars\",\n      \"mixed__in\",\n    ])\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/target_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { TargetController } from \"../../controllers/target_controller\"\n\nexport default class TargetTests extends ControllerTestCase(TargetController) {\n  fixtureHTML = `\n    <div data-controller=\"${this.identifier}\" data-${this.identifier}-connected-class=\"connected\" data-${this.identifier}-disconnected-class=\"disconnected\">\n      <div data-${this.identifier}-target=\"alpha\" id=\"alpha1\"></div>\n      <div data-${this.identifier}-target=\"alpha\" id=\"alpha2\"></div>\n      <div data-${this.identifier}-target=\"beta\" id=\"beta1\">\n        <div data-${this.identifier}-target=\"gamma\" id=\"gamma1\"></div>\n      </div>\n      <div data-controller=\"${this.identifier}\" id=\"child\">\n        <div data-${this.identifier}-target=\"delta\" id=\"delta1\"></div>\n      </div>\n      <textarea data-${this.identifier}-target=\"omega input\" id=\"input1\"></textarea>\n    </div>\n  `\n\n  \"test TargetSet#find\"() {\n    this.assert.equal(this.controller.targets.find(\"alpha\"), this.findElement(\"#alpha1\"))\n  }\n\n  \"test TargetSet#findAll\"() {\n    this.assert.deepEqual(this.controller.targets.findAll(\"alpha\"), this.findElements(\"#alpha1\", \"#alpha2\"))\n  }\n\n  \"test TargetSet#findAll with multiple arguments\"() {\n    this.assert.deepEqual(\n      this.controller.targets.findAll(\"alpha\", \"beta\"),\n      this.findElements(\"#alpha1\", \"#alpha2\", \"#beta1\")\n    )\n  }\n\n  \"test TargetSet#has\"() {\n    this.assert.equal(this.controller.targets.has(\"gamma\"), true)\n    this.assert.equal(this.controller.targets.has(\"delta\"), false)\n  }\n\n  \"test TargetSet#find ignores child controller targets\"() {\n    this.assert.equal(this.controller.targets.find(\"delta\"), null)\n    this.findElement(\"#child\").removeAttribute(\"data-controller\")\n    this.assert.equal(this.controller.targets.find(\"delta\"), this.findElement(\"#delta1\"))\n  }\n\n  \"test linked target properties\"() {\n    this.assert.equal(this.controller.betaTarget, this.findElement(\"#beta1\"))\n    this.assert.deepEqual(this.controller.betaTargets, this.findElements(\"#beta1\"))\n    this.assert.equal(this.controller.hasBetaTarget, true)\n  }\n\n  \"test inherited linked target properties\"() {\n    this.assert.equal(this.controller.alphaTarget, this.findElement(\"#alpha1\"))\n    this.assert.deepEqual(this.controller.alphaTargets, this.findElements(\"#alpha1\", \"#alpha2\"))\n  }\n\n  \"test singular linked target property throws an error when no target is found\"() {\n    this.findElement(\"#beta1\").removeAttribute(`data-${this.identifier}-target`)\n    this.assert.equal(this.controller.hasBetaTarget, false)\n    this.assert.equal(this.controller.betaTargets.length, 0)\n    this.assert.throws(() => this.controller.betaTarget)\n  }\n\n  \"test target connected callback fires after initialize() and when calling connect()\"() {\n    const connectedInputs = this.controller.inputTargets.filter((target) => target.classList.contains(\"connected\"))\n\n    this.assert.equal(connectedInputs.length, 1)\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1)\n  }\n\n  async \"test target connected callback when element is inserted\"() {\n    const connectedInput = document.createElement(\"input\")\n    connectedInput.setAttribute(`data-${this.controller.identifier}-target`, \"input\")\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1)\n\n    this.controller.element.appendChild(connectedInput)\n    await this.nextFrame\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2)\n    this.assert.ok(\n      connectedInput.classList.contains(\"connected\"),\n      `expected \"${connectedInput.className}\" to contain \"connected\"`\n    )\n    this.assert.ok(connectedInput.isConnected, \"element is present in document\")\n  }\n\n  async \"test target connected callback when present element adds the target attribute\"() {\n    const element = this.findElement(\"#alpha1\")\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1)\n\n    element.setAttribute(`data-${this.controller.identifier}-target`, \"input\")\n    await this.nextFrame\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2)\n    this.assert.ok(element.classList.contains(\"connected\"), `expected \"${element.className}\" to contain \"connected\"`)\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test target connected callback when element adds a token to an existing target attribute\"() {\n    const element = this.findElement(\"#alpha1\")\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1)\n\n    element.setAttribute(`data-${this.controller.identifier}-target`, \"alpha input\")\n    await this.nextFrame\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2)\n    this.assert.ok(element.classList.contains(\"connected\"), `expected \"${element.className}\" to contain \"connected\"`)\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test target disconnected callback fires when calling disconnect() on the controller\"() {\n    this.assert.equal(\n      this.controller.inputTargets.filter((target) => target.classList.contains(\"disconnected\")).length,\n      0\n    )\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0)\n\n    this.controller.context.disconnect()\n    await this.nextFrame\n\n    this.assert.equal(\n      this.controller.inputTargets.filter((target) => target.classList.contains(\"disconnected\")).length,\n      1\n    )\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1)\n  }\n\n  async \"test target disconnected callback when element is removed\"() {\n    const disconnectedInput = this.findElement(\"#input1\")\n\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0)\n    this.assert.notOk(\n      disconnectedInput.classList.contains(\"disconnected\"),\n      `expected \"${disconnectedInput.className}\" not to contain \"disconnected\"`\n    )\n\n    disconnectedInput.parentElement?.removeChild(disconnectedInput)\n    await this.nextFrame\n\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1)\n    this.assert.ok(\n      disconnectedInput.classList.contains(\"disconnected\"),\n      `expected \"${disconnectedInput.className}\" to contain \"disconnected\"`\n    )\n    this.assert.notOk(disconnectedInput.isConnected, \"element is not present in document\")\n  }\n\n  async \"test target disconnected callback when an element present in the document removes the target attribute\"() {\n    const element = this.findElement(\"#input1\")\n\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0)\n    this.assert.notOk(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" not to contain \"disconnected\"`\n    )\n\n    element.removeAttribute(`data-${this.controller.identifier}-target`)\n    await this.nextFrame\n\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1)\n    this.assert.ok(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" to contain \"disconnected\"`\n    )\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test target disconnected(), then connected() callback fired when the target name is present after the attribute change\"() {\n    const element = this.findElement(\"#input1\")\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1)\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0)\n    this.assert.notOk(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" not to contain \"disconnected\"`\n    )\n\n    element.setAttribute(`data-${this.controller.identifier}-target`, \"input\")\n    await this.nextFrame\n\n    this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2)\n    this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1)\n    this.assert.ok(\n      element.classList.contains(\"disconnected\"),\n      `expected \"${element.className}\" to contain \"disconnected\"`\n    )\n    this.assert.ok(element.isConnected, \"element is still present in document\")\n  }\n\n  async \"test [target]Connected() and [target]Disconnected() do not loop infinitely\"() {\n    this.controller.element.insertAdjacentHTML(\n      \"beforeend\",\n      `\n      <div data-${this.identifier}-target=\"recursive\" id=\"recursive2\"></div>\n    `\n    )\n    await this.nextFrame\n\n    this.assert.ok(!!this.fixtureElement.querySelector(\"#recursive2\"))\n    this.assert.equal(this.controller.recursiveTargetConnectedCallCountValue, 1)\n    this.assert.equal(this.controller.recursiveTargetDisconnectedCallCountValue, 0)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/value_properties_tests.ts",
    "content": "import { ValueController } from \"../../controllers/value_controller\"\nimport { ControllerTestCase } from \"../../cases/controller_test_case\"\n\nimport {\n  parseValueTypeDefault,\n  parseValueTypeConstant,\n  parseValueTypeObject,\n  parseValueTypeDefinition,\n  defaultValueForDefinition,\n} from \"../../../core/value_properties\"\n\nexport default class ValuePropertiesTests extends ControllerTestCase(ValueController) {\n  \"test parseValueTypeConstant\"() {\n    this.assert.equal(parseValueTypeConstant(String), \"string\")\n    this.assert.equal(parseValueTypeConstant(Boolean), \"boolean\")\n    this.assert.equal(parseValueTypeConstant(Array), \"array\")\n    this.assert.equal(parseValueTypeConstant(Object), \"object\")\n    this.assert.equal(parseValueTypeConstant(Number), \"number\")\n\n    this.assert.equal(parseValueTypeConstant(\"\" as any), undefined)\n    this.assert.equal(parseValueTypeConstant({} as any), undefined)\n    this.assert.equal(parseValueTypeConstant([] as any), undefined)\n    this.assert.equal(parseValueTypeConstant(true as any), undefined)\n    this.assert.equal(parseValueTypeConstant(false as any), undefined)\n    this.assert.equal(parseValueTypeConstant(0 as any), undefined)\n    this.assert.equal(parseValueTypeConstant(1 as any), undefined)\n    this.assert.equal(parseValueTypeConstant(null!), undefined)\n    this.assert.equal(parseValueTypeConstant(undefined), undefined)\n  }\n\n  \"test parseValueTypeDefault\"() {\n    this.assert.equal(parseValueTypeDefault(\"\"), \"string\")\n    this.assert.equal(parseValueTypeDefault(\"Some string\"), \"string\")\n\n    this.assert.equal(parseValueTypeDefault(true), \"boolean\")\n    this.assert.equal(parseValueTypeDefault(false), \"boolean\")\n\n    this.assert.equal(parseValueTypeDefault([]), \"array\")\n    this.assert.equal(parseValueTypeDefault([1, 2, 3]), \"array\")\n    this.assert.equal(parseValueTypeDefault([true, false, true]), \"array\")\n    this.assert.equal(parseValueTypeDefault([{}, {}, {}]), \"array\")\n\n    this.assert.equal(parseValueTypeDefault({}), \"object\")\n    this.assert.equal(parseValueTypeDefault({ one: \"key\" }), \"object\")\n\n    this.assert.equal(parseValueTypeDefault(-1), \"number\")\n    this.assert.equal(parseValueTypeDefault(0), \"number\")\n    this.assert.equal(parseValueTypeDefault(1), \"number\")\n    this.assert.equal(parseValueTypeDefault(-0.1), \"number\")\n    this.assert.equal(parseValueTypeDefault(0.0), \"number\")\n    this.assert.equal(parseValueTypeDefault(0.1), \"number\")\n\n    this.assert.equal(parseValueTypeDefault(null!), undefined)\n    this.assert.equal(parseValueTypeDefault(undefined!), undefined)\n  }\n\n  \"test parseValueTypeObject\"() {\n    const typeObject = (object: any) => {\n      return parseValueTypeObject({\n        controller: this.controller.identifier,\n        token: \"url\",\n        typeObject: object,\n      })\n    }\n\n    this.assert.equal(typeObject({ type: String, default: \"\" }), \"string\")\n    this.assert.equal(typeObject({ type: String, default: \"123\" }), \"string\")\n    this.assert.equal(typeObject({ type: String }), \"string\")\n    this.assert.equal(typeObject({ default: \"\" }), \"string\")\n    this.assert.equal(typeObject({ default: \"123\" }), \"string\")\n\n    this.assert.equal(typeObject({ type: Number, default: 0 }), \"number\")\n    this.assert.equal(typeObject({ type: Number, default: 1 }), \"number\")\n    this.assert.equal(typeObject({ type: Number, default: -1 }), \"number\")\n    this.assert.equal(typeObject({ type: Number }), \"number\")\n    this.assert.equal(typeObject({ default: 0 }), \"number\")\n    this.assert.equal(typeObject({ default: 1 }), \"number\")\n    this.assert.equal(typeObject({ default: -1 }), \"number\")\n\n    this.assert.equal(typeObject({ type: Array, default: [] }), \"array\")\n    this.assert.equal(typeObject({ type: Array, default: [1] }), \"array\")\n    this.assert.equal(typeObject({ type: Array }), \"array\")\n    this.assert.equal(typeObject({ default: [] }), \"array\")\n    this.assert.equal(typeObject({ default: [1] }), \"array\")\n\n    this.assert.equal(typeObject({ type: Object, default: {} }), \"object\")\n    this.assert.equal(typeObject({ type: Object, default: { some: \"key\" } }), \"object\")\n    this.assert.equal(typeObject({ type: Object }), \"object\")\n    this.assert.equal(typeObject({ default: {} }), \"object\")\n    this.assert.equal(typeObject({ default: { some: \"key\" } }), \"object\")\n\n    this.assert.equal(typeObject({ type: Boolean, default: true }), \"boolean\")\n    this.assert.equal(typeObject({ type: Boolean, default: false }), \"boolean\")\n    this.assert.equal(typeObject({ type: Boolean }), \"boolean\")\n    this.assert.equal(typeObject({ default: false }), \"boolean\")\n\n    this.assert.throws(() => typeObject({ type: Boolean, default: \"something else\" }), {\n      name: \"Error\",\n      message: `The specified default value for the Stimulus Value \"test.url\" must match the defined type \"boolean\". The provided default value of \"something else\" is of type \"string\".`,\n    })\n\n    this.assert.throws(() => typeObject({ type: Boolean, default: \"true\" }), {\n      name: \"Error\",\n      message: `The specified default value for the Stimulus Value \"test.url\" must match the defined type \"boolean\". The provided default value of \"true\" is of type \"string\".`,\n    })\n  }\n\n  \"test parseValueTypeDefinition booleans\"() {\n    const typeDefinition = (definition: any) => {\n      return parseValueTypeDefinition({\n        controller: this.controller.identifier,\n        token: \"url\",\n        typeDefinition: definition,\n      })\n    }\n\n    this.assert.equal(typeDefinition(Boolean), \"boolean\")\n    this.assert.equal(typeDefinition(true), \"boolean\")\n    this.assert.equal(typeDefinition(false), \"boolean\")\n    this.assert.equal(typeDefinition({ type: Boolean, default: false }), \"boolean\")\n    this.assert.equal(typeDefinition({ type: Boolean }), \"boolean\")\n    this.assert.equal(typeDefinition({ default: true }), \"boolean\")\n\n    // since the provided value is actually an object, it's going to be of type \"object\"\n    this.assert.equal(typeDefinition({ default: null }), \"object\")\n    this.assert.equal(typeDefinition({ default: undefined }), \"object\")\n\n    this.assert.equal(typeDefinition({}), \"object\")\n    this.assert.equal(typeDefinition(\"\"), \"string\")\n    this.assert.equal(typeDefinition([]), \"array\")\n\n    this.assert.throws(() => typeDefinition(null))\n    this.assert.throws(() => typeDefinition(undefined))\n  }\n\n  \"test defaultValueForDefinition\"() {\n    this.assert.deepEqual(defaultValueForDefinition(String), \"\")\n    this.assert.deepEqual(defaultValueForDefinition(Boolean), false)\n    this.assert.deepEqual(defaultValueForDefinition(Object), {})\n    this.assert.deepEqual(defaultValueForDefinition(Array), [])\n    this.assert.deepEqual(defaultValueForDefinition(Number), 0)\n\n    this.assert.deepEqual(defaultValueForDefinition({ type: String }), \"\")\n    this.assert.deepEqual(defaultValueForDefinition({ type: Boolean }), false)\n    this.assert.deepEqual(defaultValueForDefinition({ type: Object }), {})\n    this.assert.deepEqual(defaultValueForDefinition({ type: Array }), [])\n    this.assert.deepEqual(defaultValueForDefinition({ type: Number }), 0)\n\n    this.assert.deepEqual(defaultValueForDefinition({ type: String, default: null }), null)\n    this.assert.deepEqual(defaultValueForDefinition({ type: Boolean, default: null }), null)\n    this.assert.deepEqual(defaultValueForDefinition({ type: Object, default: null }), null)\n    this.assert.deepEqual(defaultValueForDefinition({ type: Array, default: null }), null)\n    this.assert.deepEqual(defaultValueForDefinition({ type: Number, default: null }), null)\n\n    this.assert.deepEqual(defaultValueForDefinition({ type: String, default: \"some string\" }), \"some string\")\n    this.assert.deepEqual(defaultValueForDefinition({ type: Boolean, default: true }), true)\n    this.assert.deepEqual(defaultValueForDefinition({ type: Object, default: { some: \"key\" } }), { some: \"key\" })\n    this.assert.deepEqual(defaultValueForDefinition({ type: Array, default: [1, 2, 3] }), [1, 2, 3])\n    this.assert.deepEqual(defaultValueForDefinition({ type: Number, default: 99 }), 99)\n\n    this.assert.deepEqual(defaultValueForDefinition(\"some string\"), \"some string\")\n    this.assert.deepEqual(defaultValueForDefinition(true), true)\n    this.assert.deepEqual(defaultValueForDefinition({ some: \"key\" }), { some: \"key\" })\n    this.assert.deepEqual(defaultValueForDefinition([1, 2, 3]), [1, 2, 3])\n    this.assert.deepEqual(defaultValueForDefinition(99), 99)\n\n    this.assert.deepEqual(defaultValueForDefinition({ default: \"some string\" }), \"some string\")\n    this.assert.deepEqual(defaultValueForDefinition({ default: true }), true)\n    this.assert.deepEqual(defaultValueForDefinition({ default: { some: \"key\" } }), { some: \"key\" })\n    this.assert.deepEqual(defaultValueForDefinition({ default: [1, 2, 3] }), [1, 2, 3])\n    this.assert.deepEqual(defaultValueForDefinition({ default: 99 }), 99)\n\n    this.assert.deepEqual(defaultValueForDefinition({ default: null }), null)\n    this.assert.deepEqual(defaultValueForDefinition({ default: undefined }), undefined)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/core/value_tests.ts",
    "content": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { ValueController } from \"../../controllers/value_controller\"\n\nexport default class ValueTests extends ControllerTestCase(ValueController) {\n  fixtureHTML = `\n    <div data-controller=\"${this.identifier}\"\n      data-${this.identifier}-shadowed-boolean-value=\"true\"\n      data-${this.identifier}-numeric-value=\"123\"\n      data-${this.identifier}-string-value=\"ok\"\n      data-${this.identifier}-ids-value=\"[1,2,3]\"\n      data-${this.identifier}-options-value='{\"one\":[2,3]}'\n      data-${this.identifier}-time-24hr-value=\"true\">\n    </div>\n  `\n\n  \"test string values\"() {\n    this.assert.deepEqual(this.controller.stringValue, \"ok\")\n\n    this.controller.stringValue = \"cool\"\n    this.assert.deepEqual(this.controller.stringValue, \"cool\")\n    this.assert.deepEqual(this.get(\"string-value\"), \"cool\")\n  }\n\n  \"test numeric values\"() {\n    this.assert.deepEqual(this.controller.numericValue, 123)\n\n    this.controller.numericValue = 456\n    this.assert.deepEqual(this.controller.numericValue, 456)\n    this.assert.deepEqual(this.get(\"numeric-value\"), \"456\")\n\n    this.controller.numericValue = \"789\" as any\n    this.assert.deepEqual(this.controller.numericValue, 789)\n\n    this.controller.numericValue = 1.23\n    this.assert.deepEqual(this.controller.numericValue, 1.23)\n    this.assert.deepEqual(this.get(\"numeric-value\"), \"1.23\")\n\n    this.controller.numericValue = Infinity\n    this.assert.deepEqual(this.controller.numericValue, Infinity)\n    this.assert.deepEqual(this.get(\"numeric-value\"), \"Infinity\")\n\n    this.controller.numericValue = \"garbage\" as any\n    this.assert.ok(isNaN(this.controller.numericValue))\n    this.assert.equal(this.get(\"numeric-value\"), \"garbage\")\n\n    this.controller.numericValue = \"\" as any\n    this.assert.equal(this.controller.numericValue, 0)\n    this.assert.equal(this.get(\"numeric-value\"), \"\")\n\n    // Number values should support Numeric separators\n    this.set(\"numeric-value\", \"7_150\")\n    this.assert.equal(this.controller.numericValue, 7150)\n\n    // Number values should be written simply, without Numeric separators\n    this.controller.numericValue = 10500\n    this.assert.deepEqual(this.get(\"numeric-value\"), \"10500\")\n  }\n\n  \"test boolean values\"() {\n    this.assert.deepEqual(this.controller.shadowedBooleanValue, true)\n\n    this.controller.shadowedBooleanValue = false\n    this.assert.deepEqual(this.controller.shadowedBooleanValue, false)\n    this.assert.deepEqual(this.get(\"shadowed-boolean-value\"), \"false\")\n\n    this.controller.shadowedBooleanValue = \"\" as any\n    this.assert.deepEqual(this.controller.shadowedBooleanValue, true)\n    this.assert.deepEqual(this.get(\"shadowed-boolean-value\"), \"\")\n\n    this.controller.shadowedBooleanValue = 0 as any\n    this.assert.deepEqual(this.controller.shadowedBooleanValue, false)\n    this.assert.deepEqual(this.get(\"shadowed-boolean-value\"), \"0\")\n\n    this.controller.shadowedBooleanValue = 1 as any\n    this.assert.deepEqual(this.controller.shadowedBooleanValue, true)\n    this.assert.deepEqual(this.get(\"shadowed-boolean-value\"), \"1\")\n\n    this.controller.shadowedBooleanValue = \"False\" as any\n    this.assert.deepEqual(this.controller.shadowedBooleanValue, false)\n    this.assert.deepEqual(this.get(\"shadowed-boolean-value\"), \"False\")\n  }\n\n  \"test array values\"() {\n    this.assert.deepEqual(this.controller.idsValue, [1, 2, 3])\n\n    this.controller.idsValue.push(4)\n    this.assert.deepEqual(this.controller.idsValue, [1, 2, 3])\n\n    this.controller.idsValue = []\n    this.assert.deepEqual(this.controller.idsValue, [])\n    this.assert.deepEqual(this.get(\"ids-value\"), \"[]\")\n\n    this.controller.idsValue = null as any\n    this.assert.throws(() => this.controller.idsValue)\n\n    this.controller.idsValue = {} as any\n    this.assert.throws(() => this.controller.idsValue)\n  }\n\n  \"test object values\"() {\n    this.assert.deepEqual(this.controller.optionsValue, { one: [2, 3] })\n\n    this.controller.optionsValue[\"one\"] = 0\n    this.assert.deepEqual(this.controller.optionsValue, { one: [2, 3] })\n\n    this.controller.optionsValue = {}\n    this.assert.deepEqual(this.controller.optionsValue, {})\n    this.assert.deepEqual(this.get(\"options-value\"), \"{}\")\n\n    this.controller.optionsValue = null as any\n    this.assert.throws(() => this.controller.optionsValue)\n\n    this.controller.optionsValue = [] as any\n    this.assert.throws(() => this.controller.optionsValue)\n  }\n\n  \"test accessing a string value returns the empty string when the attribute is missing\"() {\n    this.controller.stringValue = undefined as any\n    this.assert.notOk(this.has(\"string-value\"))\n    this.assert.deepEqual(this.controller.stringValue, \"\")\n  }\n\n  \"test accessing a numeric value returns zero when the attribute is missing\"() {\n    this.controller.numericValue = undefined as any\n    this.assert.notOk(this.has(\"numeric-value\"))\n    this.assert.deepEqual(this.controller.numericValue, 0)\n  }\n\n  \"test accessing a boolean value returns false when the attribute is missing\"() {\n    this.controller.shadowedBooleanValue = undefined as any\n    this.assert.notOk(this.has(\"shadowed-boolean-value\"))\n    this.assert.deepEqual(this.controller.shadowedBooleanValue, false)\n  }\n\n  \"test accessing an array value returns an empty array when the attribute is missing\"() {\n    this.controller.idsValue = undefined as any\n    this.assert.notOk(this.has(\"ids-value\"))\n    this.assert.deepEqual(this.controller.idsValue, [])\n\n    this.controller.idsValue.push(1)\n    this.assert.deepEqual(this.controller.idsValue, [])\n  }\n\n  \"test accessing an object value returns an empty object when the attribute is missing\"() {\n    this.controller.optionsValue = undefined as any\n    this.assert.notOk(this.has(\"options-value\"))\n    this.assert.deepEqual(this.controller.optionsValue, {})\n\n    this.controller.optionsValue.hello = true\n    this.assert.deepEqual(this.controller.optionsValue, {})\n  }\n\n  async \"test changed callbacks\"() {\n    this.assert.deepEqual(this.controller.loggedNumericValues, [123])\n    this.assert.deepEqual(this.controller.oldLoggedNumericValues, [0])\n\n    this.controller.numericValue = 0\n    await this.nextFrame\n    this.assert.deepEqual(this.controller.loggedNumericValues, [123, 0])\n    this.assert.deepEqual(this.controller.oldLoggedNumericValues, [0, 123])\n\n    this.set(\"numeric-value\", \"1\")\n    await this.nextFrame\n    this.assert.deepEqual(this.controller.loggedNumericValues, [123, 0, 1])\n    this.assert.deepEqual(this.controller.oldLoggedNumericValues, [0, 123, 0])\n  }\n\n  async \"test changed callbacks for object\"() {\n    this.assert.deepEqual(this.controller.optionsValues, [{ one: [2, 3] }])\n    this.assert.deepEqual(this.controller.oldOptionsValues, [{}])\n\n    this.controller.optionsValue = { person: { name: \"John\", age: 42, active: true } }\n    await this.nextFrame\n    this.assert.deepEqual(this.controller.optionsValues, [\n      { one: [2, 3] },\n      { person: { name: \"John\", age: 42, active: true } },\n    ])\n    this.assert.deepEqual(this.controller.oldOptionsValues, [{}, { one: [2, 3] }])\n\n    this.set(\"options-value\", \"{}\")\n    await this.nextFrame\n    this.assert.deepEqual(this.controller.optionsValues, [\n      { one: [2, 3] },\n      { person: { name: \"John\", age: 42, active: true } },\n      {},\n    ])\n    this.assert.deepEqual(this.controller.oldOptionsValues, [\n      {},\n      { one: [2, 3] },\n      { person: { name: \"John\", age: 42, active: true } },\n    ])\n  }\n\n  async \"test default values trigger changed callbacks\"() {\n    this.assert.deepEqual(this.controller.loggedMissingStringValues, [\"\"])\n    this.assert.deepEqual(this.controller.oldLoggedMissingStringValues, [undefined])\n\n    this.controller.missingStringValue = \"hello\"\n    await this.nextFrame\n    this.assert.deepEqual(this.controller.loggedMissingStringValues, [\"\", \"hello\"])\n    this.assert.deepEqual(this.controller.oldLoggedMissingStringValues, [undefined, \"\"])\n\n    this.controller.missingStringValue = undefined as any\n    await this.nextFrame\n    this.assert.deepEqual(this.controller.loggedMissingStringValues, [\"\", \"hello\", \"\"])\n    this.assert.deepEqual(this.controller.oldLoggedMissingStringValues, [undefined, \"\", \"hello\"])\n  }\n\n  \"test keys may be specified in kebab-case\"() {\n    this.assert.equal(this.controller.time24hrValue, true)\n  }\n\n  has(name: string) {\n    return this.element.hasAttribute(this.attr(name))\n  }\n\n  get(name: string) {\n    return this.element.getAttribute(this.attr(name))\n  }\n\n  set(name: string, value: string) {\n    return this.element.setAttribute(this.attr(name), value)\n  }\n\n  attr(name: string) {\n    return `data-${this.identifier}-${name}`\n  }\n\n  get element() {\n    return this.controller.element\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/mutation-observers/attribute_observer_tests.ts",
    "content": "import { AttributeObserver, AttributeObserverDelegate } from \"../../../mutation-observers/attribute_observer\"\nimport { ObserverTestCase } from \"../../cases/observer_test_case\"\n\nexport default class AttributeObserverTests extends ObserverTestCase implements AttributeObserverDelegate {\n  attributeName = \"data-test\"\n  fixtureHTML = `<div id=\"outer\" ${this.attributeName}><div id=\"inner\"></div></div>`\n  observer = new AttributeObserver(this.fixtureElement, this.attributeName, this)\n\n  async \"test elementMatchedAttribute\"() {\n    this.assert.deepEqual(this.calls, [[\"elementMatchedAttribute\", this.outerElement, this.attributeName]])\n  }\n\n  async \"test elementAttributeValueChanged\"() {\n    this.outerElement.setAttribute(this.attributeName, \"hello\")\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [\n      [\"elementMatchedAttribute\", this.outerElement, this.attributeName],\n      [\"elementAttributeValueChanged\", this.outerElement, this.attributeName],\n    ])\n  }\n\n  async \"test elementUnmatchedAttribute\"() {\n    this.outerElement.removeAttribute(this.attributeName)\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [\n      [\"elementMatchedAttribute\", this.outerElement, this.attributeName],\n      [\"elementUnmatchedAttribute\", this.outerElement, this.attributeName],\n    ])\n  }\n\n  async \"test observes attribute changes to child elements\"() {\n    this.innerElement.setAttribute(this.attributeName, \"hello\")\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [\n      [\"elementMatchedAttribute\", this.outerElement, this.attributeName],\n      [\"elementMatchedAttribute\", this.innerElement, this.attributeName],\n    ])\n  }\n\n  async \"test ignores other attributes\"() {\n    this.outerElement.setAttribute(this.attributeName + \"-x\", \"hello\")\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [[\"elementMatchedAttribute\", this.outerElement, this.attributeName]])\n  }\n\n  async \"test observes removal of nested matched element HTML\"() {\n    const { innerElement, outerElement } = this\n\n    innerElement.setAttribute(this.attributeName, \"\")\n    await this.nextFrame\n\n    this.fixtureElement.innerHTML = \"\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [\n      [\"elementMatchedAttribute\", outerElement, this.attributeName],\n      [\"elementMatchedAttribute\", innerElement, this.attributeName],\n      [\"elementUnmatchedAttribute\", outerElement, this.attributeName],\n      [\"elementUnmatchedAttribute\", innerElement, this.attributeName],\n    ])\n  }\n\n  async \"test ignores synchronously disconnected elements\"() {\n    const { innerElement, outerElement } = this\n\n    outerElement.removeChild(innerElement)\n    innerElement.setAttribute(this.attributeName, \"\")\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [[\"elementMatchedAttribute\", outerElement, this.attributeName]])\n  }\n\n  async \"test ignores synchronously moved elements\"() {\n    const { innerElement, outerElement } = this\n\n    document.body.appendChild(innerElement)\n    innerElement.setAttribute(this.attributeName, \"\")\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [[\"elementMatchedAttribute\", outerElement, this.attributeName]])\n\n    document.body.removeChild(innerElement)\n  }\n\n  get outerElement() {\n    return this.findElement(\"#outer\")\n  }\n\n  get innerElement() {\n    return this.findElement(\"#inner\")\n  }\n\n  // Attribute observer delegate\n\n  elementMatchedAttribute(element: Element, attributeName: string) {\n    this.recordCall(\"elementMatchedAttribute\", element, attributeName)\n  }\n\n  elementAttributeValueChanged(element: Element, attributeName: string) {\n    this.recordCall(\"elementAttributeValueChanged\", element, attributeName)\n  }\n\n  elementUnmatchedAttribute(element: Element, attributeName: string) {\n    this.recordCall(\"elementUnmatchedAttribute\", element, attributeName)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/mutation-observers/selector_observer_tests.ts",
    "content": "import { SelectorObserver, SelectorObserverDelegate } from \"../../../mutation-observers/selector_observer\"\nimport { ObserverTestCase } from \"../../cases/observer_test_case\"\n\nexport default class SelectorObserverTests extends ObserverTestCase implements SelectorObserverDelegate {\n  attributeName = \"data-test\"\n  selector = \"div[data-test~=two]\"\n  details = { some: \"details\" }\n\n  fixtureHTML = `\n    <div id=\"container\" ${this.attributeName}=\"one two\">\n      <div id=\"div1\" ${this.attributeName}=\"one\"></div>\n      <div id=\"div2\" ${this.attributeName}=\"two\"></div>\n      <span id=\"span1\" ${this.attributeName}=\"one\"></span>\n      <span id=\"span2\" ${this.attributeName}=\"two\"></span>\n    </div>\n  `\n  observer = new SelectorObserver(this.fixtureElement, this.selector, this, this.details)\n\n  async \"test should match when observer starts\"() {\n    this.assert.deepEqual(this.calls, [\n      [\"selectorMatched\", this.element, this.selector, this.details],\n      [\"selectorMatched\", this.div2, this.selector, this.details],\n    ])\n  }\n\n  async \"test should match when element gets appended\"() {\n    const element1 = document.createElement(\"div\")\n    const element2 = document.createElement(\"div\")\n\n    element1.dataset.test = \"one two\"\n    element2.dataset.test = \"three four\"\n\n    this.element.appendChild(element1)\n    this.element.appendChild(element2)\n\n    await this.nextFrame\n\n    this.assert.deepEqual(this.calls, [\n      [\"selectorMatched\", this.element, this.selector, this.details],\n      [\"selectorMatched\", this.div2, this.selector, this.details],\n      [\"selectorMatched\", element1, this.selector, this.details],\n    ])\n  }\n\n  async \"test should not match/unmatch when the attribute gets updated and matching selector persists\"() {\n    this.element.setAttribute(this.attributeName, \"two three\")\n\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [])\n  }\n\n  async \"test should match when attribute gets updated and start to matche selector\"() {\n    this.div1.setAttribute(this.attributeName, \"updated two\")\n\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [[\"selectorMatched\", this.div1, this.selector, this.details]])\n  }\n\n  async \"test should unmatch when attribute gets updated but matching attribute value gets removed\"() {\n    this.div2.setAttribute(this.attributeName, \"updated\")\n\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [[\"selectorUnmatched\", this.div2, this.selector, this.details]])\n  }\n\n  async \"test should unmatch when attribute gets removed\"() {\n    this.element.removeAttribute(this.attributeName)\n    this.div2.removeAttribute(this.attributeName)\n\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"selectorUnmatched\", this.element, this.selector, this.details],\n      [\"selectorUnmatched\", this.div2, this.selector, this.details],\n    ])\n  }\n\n  async \"test should unmatch when element gets removed\"() {\n    const element = this.element\n    const div1 = this.div1\n    const div2 = this.div2\n\n    element.remove()\n    div1.remove()\n    div2.remove()\n\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"selectorUnmatched\", element, this.selector, this.details],\n      [\"selectorUnmatched\", div2, this.selector, this.details],\n    ])\n  }\n\n  async \"test should not match/unmatch when observer is paused\"() {\n    this.observer.pause(() => {\n      this.div2.remove()\n\n      const element = document.createElement(\"div\")\n      element.dataset.test = \"one two\"\n      this.element.appendChild(element)\n    })\n\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [])\n  }\n\n  get element(): Element {\n    return this.findElement(\"#container\")\n  }\n\n  get div1(): Element {\n    return this.findElement(\"#div1\")\n  }\n\n  get div2(): Element {\n    return this.findElement(\"#div2\")\n  }\n\n  // Selector observer delegate\n\n  selectorMatched(element: Element, selector: string, details: object) {\n    this.recordCall(\"selectorMatched\", element, selector, details)\n  }\n\n  selectorUnmatched(element: Element, selector: string, details: object) {\n    this.recordCall(\"selectorUnmatched\", element, selector, details)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/mutation-observers/token_list_observer_tests.ts",
    "content": "import { Token, TokenListObserver, TokenListObserverDelegate } from \"../../../mutation-observers/token_list_observer\"\nimport { ObserverTestCase } from \"../../cases/observer_test_case\"\n\nexport default class TokenListObserverTests extends ObserverTestCase implements TokenListObserverDelegate {\n  attributeName = \"data-test\"\n  fixtureHTML = `<div ${this.attributeName}=\"one two\"></div>`\n  observer = new TokenListObserver(this.fixtureElement, this.attributeName, this)\n\n  async \"test tokenMatched\"() {\n    this.assert.deepEqual(this.calls, [\n      [\"tokenMatched\", this.element, this.attributeName, \"one\", 0],\n      [\"tokenMatched\", this.element, this.attributeName, \"two\", 1],\n    ])\n  }\n\n  async \"test adding a token to the right\"() {\n    this.tokenString = \"one two three\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [[\"tokenMatched\", this.element, this.attributeName, \"three\", 2]])\n  }\n\n  async \"test inserting a token in the middle\"() {\n    this.tokenString = \"one three two\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"tokenUnmatched\", this.element, this.attributeName, \"two\", 1],\n      [\"tokenMatched\", this.element, this.attributeName, \"three\", 1],\n      [\"tokenMatched\", this.element, this.attributeName, \"two\", 2],\n    ])\n  }\n\n  async \"test removing the leftmost token\"() {\n    this.tokenString = \"two\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"tokenUnmatched\", this.element, this.attributeName, \"one\", 0],\n      [\"tokenUnmatched\", this.element, this.attributeName, \"two\", 1],\n      [\"tokenMatched\", this.element, this.attributeName, \"two\", 0],\n    ])\n  }\n\n  async \"test removing the rightmost token\"() {\n    this.tokenString = \"one\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [[\"tokenUnmatched\", this.element, this.attributeName, \"two\", 1]])\n  }\n\n  async \"test removing the only token\"() {\n    this.tokenString = \"one\"\n    await this.nextFrame\n    this.tokenString = \"\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"tokenUnmatched\", this.element, this.attributeName, \"two\", 1],\n      [\"tokenUnmatched\", this.element, this.attributeName, \"one\", 0],\n    ])\n  }\n\n  get element(): Element {\n    return this.findElement(\"div\")\n  }\n\n  set tokenString(value: string) {\n    this.element.setAttribute(this.attributeName, value)\n  }\n\n  // Token observer delegate\n\n  tokenMatched(token: Token) {\n    this.recordCall(\"tokenMatched\", token.element, token.attributeName, token.content, token.index)\n  }\n\n  tokenUnmatched(token: Token) {\n    this.recordCall(\"tokenUnmatched\", token.element, token.attributeName, token.content, token.index)\n  }\n}\n"
  },
  {
    "path": "src/tests/modules/mutation-observers/value_list_observer_tests.ts",
    "content": "import { Token, ValueListObserver, ValueListObserverDelegate } from \"../../../mutation-observers\"\nimport { ObserverTestCase } from \"../../cases/observer_test_case\"\n\nexport interface Value {\n  id: number\n  token: Token\n}\n\nexport default class ValueListObserverTests extends ObserverTestCase implements ValueListObserverDelegate<Value> {\n  attributeName = \"data-test\"\n  fixtureHTML = `<div ${this.attributeName}=\"one\"></div>`\n  observer = new ValueListObserver(this.fixtureElement, this.attributeName, this)\n  lastValueId = 0\n\n  async \"test elementMatchedValue\"() {\n    this.assert.deepEqual(this.calls, [[\"elementMatchedValue\", this.element, 1, \"one\"]])\n  }\n\n  async \"test adding a token to the right\"() {\n    this.valueString = \"one two\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [[\"elementMatchedValue\", this.element, 2, \"two\"]])\n  }\n\n  async \"test adding a token to the left\"() {\n    this.valueString = \"two one\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"elementUnmatchedValue\", this.element, 1, \"one\"],\n      [\"elementMatchedValue\", this.element, 2, \"two\"],\n      [\"elementMatchedValue\", this.element, 3, \"one\"],\n    ])\n  }\n\n  async \"test removing a token from the right\"() {\n    this.valueString = \"one two\"\n    await this.nextFrame\n    this.valueString = \"one\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"elementMatchedValue\", this.element, 2, \"two\"],\n      [\"elementUnmatchedValue\", this.element, 2, \"two\"],\n    ])\n  }\n\n  async \"test removing a token from the left\"() {\n    this.valueString = \"one two\"\n    await this.nextFrame\n    this.valueString = \"two\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"elementMatchedValue\", this.element, 2, \"two\"],\n      [\"elementUnmatchedValue\", this.element, 1, \"one\"],\n      [\"elementUnmatchedValue\", this.element, 2, \"two\"],\n      [\"elementMatchedValue\", this.element, 3, \"two\"],\n    ])\n  }\n\n  async \"test removing the only token\"() {\n    this.valueString = \"\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [[\"elementUnmatchedValue\", this.element, 1, \"one\"]])\n  }\n\n  async \"test removing and re-adding a token produces a new value\"() {\n    this.valueString = \"\"\n    await this.nextFrame\n    this.valueString = \"one\"\n    await this.nextFrame\n\n    this.assert.deepEqual(this.testCalls, [\n      [\"elementUnmatchedValue\", this.element, 1, \"one\"],\n      [\"elementMatchedValue\", this.element, 2, \"one\"],\n    ])\n  }\n\n  get element() {\n    return this.findElement(\"div\")\n  }\n\n  set valueString(value: string) {\n    this.element.setAttribute(this.attributeName, value)\n  }\n\n  // Value observer delegate\n\n  parseValueForToken(token: Token) {\n    return { id: ++this.lastValueId, token }\n  }\n\n  elementMatchedValue(element: Element, value: Value) {\n    this.recordCall(\"elementMatchedValue\", element, value.id, value.token.content)\n  }\n\n  elementUnmatchedValue(element: Element, value: Value) {\n    this.recordCall(\"elementUnmatchedValue\", element, value.id, value.token.content)\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [ \"dom\", \"dom.iterable\", \"es2015\", \"scripthost\" ],\n    \"module\": \"es2015\",\n    \"moduleResolution\": \"node\",\n    \"noUnusedLocals\": true,\n    \"rootDir\": \"src\",\n    \"strict\": true,\n    \"target\": \"es2017\",\n    \"removeComments\": true,\n    \"outDir\": \"dist\",\n    \"baseUrl\": \".\",\n    \"noEmit\": false,\n    \"declaration\": false\n  },\n  \"exclude\": [\n    \"dist\"\n  ],\n  \"include\": [\n    \"src/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.test.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"es5\"\n  }\n}\n"
  }
]