├── .dockerignore
├── .editorconfig
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── config.yml
│ └── feature_request.yml
├── SECURITY.md
└── workflows
│ ├── playwright.yml
│ └── tests.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── eslint.config.mjs
├── index.html
├── package-lock.json
├── package.json
├── packages
└── signalizejs
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── Signalize.js
│ └── modules
│ │ ├── ajax.js
│ │ ├── bind.js
│ │ ├── component.js
│ │ ├── dialog.js
│ │ ├── directives.js
│ │ ├── directives
│ │ ├── for.js
│ │ └── if.js
│ │ ├── dom
│ │ ├── ready.js
│ │ └── traverser.js
│ │ ├── evaluator.js
│ │ ├── event.js
│ │ ├── hyperscript.js
│ │ ├── intersection-observer.js
│ │ ├── logger.js
│ │ ├── mutation-observer.js
│ │ ├── offset.js
│ │ ├── scope.js
│ │ ├── signal.js
│ │ ├── sizes.js
│ │ ├── snippets.js
│ │ ├── spa.js
│ │ ├── strings
│ │ └── cases.js
│ │ ├── task.js
│ │ ├── viewport.js
│ │ └── visibility.js
│ ├── tests
│ ├── height.spec.mjs
│ ├── pages
│ │ ├── height.html
│ │ └── width.html
│ └── width.spec.mjs
│ └── types
│ ├── Signalize.d.ts
│ ├── index.d.ts
│ └── modules
│ ├── ajax.d.ts
│ ├── bind.d.ts
│ ├── component.d.ts
│ ├── dialog.d.ts
│ ├── directives.d.ts
│ ├── dom
│ ├── ready.d.ts
│ └── traverser.d.ts
│ ├── evaluator.d.ts
│ ├── event.d.ts
│ ├── hyperscript.d.ts
│ ├── intersection-observer.d.ts
│ ├── logger.d.ts
│ ├── mutation-observer.d.ts
│ ├── offset.d.ts
│ ├── scope.d.ts
│ ├── signal.d.ts
│ ├── sizes.d.ts
│ ├── snippets.d.ts
│ ├── spa.d.ts
│ ├── strings
│ └── cases.d.ts
│ ├── task.d.ts
│ ├── viewport.d.ts
│ └── visibility.ts
├── playwright.config.mjs
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Root
2 | /node_modules/
3 | *.tgz
4 | /coverage
5 | .idea
6 | /.idea
7 |
8 | #Packages
9 | /packages/**/node_modules
10 | /packages/**/dist
11 | /packages/**/esm
12 | /packages/**/lib
13 | /packages/**/tmp
14 | /packages/**/types
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = tab
8 | indent_size = 4
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.{yml,yaml}]
14 | indent_style = space
15 | indent_size = 4
16 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | dev@signalizejs.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Signalize Contributing Guide
2 |
3 | Hi! I'm excited that you are interested in contributing to Signalize.
4 | Before submitting your contribution, please make sure to take a moment and read through the following guidelines:
5 |
6 | - [Code of Conduct](https://github.com/signalizejs/signalize/blob/master/.github/CODE_OF_CONDUCT.md)
7 | - [Pull Request Guidelines](#pull-request-guidelines)
8 | - [Development Setup](#development-setup)
9 | - [Project Structure](#project-structure)
10 |
11 | ## Issue Reporting Guidelines
12 |
13 | - Always use [Github Issues](https://github.com/signalizejs/signalize/issues) to create new issues.
14 | - In case of discussion, checkout [Github Discussions](https://github.com/signalizejs/signalize/discussions) or our [Discord Channel](https://discord.gg/NuJsk5SMDz).
15 |
16 | ## Pull Request Guidelines
17 |
18 | - Submit pull requests against the `maain` branch
19 | - It's OK to have multiple small commits as you work on the PR. Try to split them into logical units (so the changes in commit makes sense).
20 | - Make sure `tests` passes (see [development setup](#development-setup))
21 |
22 | - If adding a new feature:
23 | - Add accompanying test case.
24 | - Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
25 |
26 | - If fixing bug:
27 | - If you are resolving a special issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `Update something (fix #3899)`.
28 | - Provide a detailed description of the bug in the PR. Live demo preferred.
29 | - Add appropriate test coverage if applicable.
30 |
31 | ## Development Setup
32 | - You will [Node.js](http://nodejs.org) >= 22 and [Yarn](https://yarnpkg.com/)
33 | - After cloning the repo, run `yarn i`. This will install dependencies
34 | - You can use `Docker Setup` in this repository through `Docker Compose` or `Visual Studio Dev Containers`.
35 |
36 | ### Committing Changes
37 |
38 | - Commit messages should be self explanatory
39 | - Avoid messages like `Fix, Clenup, Revert, Change, Tunning` and similar.
40 |
41 | ### NPM scripts
42 | - There are following tasks defined in the root `package.json`:
43 | - **repo:init**: This will initialize the repository
44 | - **repo:install-playwright**: This will install playwright
45 | - **eslint:check**: This run's eslint check
46 | - **eslint:fix**: This run's eslint check but fixes auto-fixable issues
47 | - **tests:run**: This will run playwright tests
48 | - **tests:report**: This will show test rsults
49 |
50 | ## Project Structure
51 |
52 | - **`packages`**: Contains all packages
53 | - **`packages/*/tests`**: Contains tests for a specific package
54 | - **`packages/*/src`**: Source code of that package
55 | - **`packages/*/types`**: Typescript types
56 |
57 | ## Financial Contribution
58 |
59 | In case you use Signalize or like the idea, you can also contribute financially on [Sponsor Page](https://github.com/sponsors/Machy8). Every donation is more then welcome :).
60 |
61 | ## Credits
62 |
63 | Thank you to [all the people who have already contributed](https://github.com/signalizejs/signalize/graphs/contributors) to Signalize!
64 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [Machy8]
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: "🐞 Bug report"
2 | description: Report an issue with SignalizeJS.
3 | labels: ["bug"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for taking the time to fill out this bug report!
9 | - type: textarea
10 | id: bug-description
11 | attributes:
12 | label: Describe the bug
13 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
14 | placeholder: Bug description
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: reproduction
19 | attributes:
20 | label: Reproduction
21 | description: Please provide a link to a repo or REPL that can reproduce the problem you ran into. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "need reproduction" label. If no reproduction is provided within a reasonable time-frame, the issue will be closed.
22 | placeholder: Reproduction
23 | validations:
24 | required: true
25 | - type: textarea
26 | id: logs
27 | attributes:
28 | label: Logs
29 | description: "Please include browser console and server logs around the time this bug occurred. Optional if provided reproduction. Please try not to insert an image but copy paste the log text."
30 | render: shell
31 | - type: textarea
32 | id: system-info
33 | attributes:
34 | label: System Info
35 | description: Which version of SignalizeJS you have used. Which SignalizeJS package you use. Which version of Node / if you use SignalizeJS via CDN, which browser and what is the browser version.
36 | render: shell
37 | placeholder: System, Binaries, Browsers
38 | validations:
39 | required: true
40 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: ❤️ Sponsor
4 | url: https://github.com/sponsors/Machy8
5 | about: Do you like SignalizeJS? I will be happy if you buy me a coffee once in a while ☕.
6 | - name: ✉️ Discussion on Github
7 | url: https://github.com/signalizejs/signalize/discussions
8 | about: Ask questions and discuss with other SignalizeJS users.
9 | - name: 💬 Discord Chat
10 | url: https://discord.gg/V82TvAVRKY
11 | about: Ask questions and discuss with other Signalize users in real time.
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: "💎 Feature Request"
2 | description: Request a new Signalize feature.
3 | labels: [feature]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for taking the time to request this feature!
9 | - type: textarea
10 | id: problem
11 | attributes:
12 | label: Describe the problem
13 | description: Please provide a clear and concise description the problem this feature would solve. The more information you can provide here, the better.
14 | placeholder: I'm always frustrated when...
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: solution
19 | attributes:
20 | label: Describe the proposed solution
21 | description: Please provide a clear and concise description of what you would like to happen.
22 | placeholder: I would like to see...
23 | validations:
24 | required: true
25 | - type: textarea
26 | id: alternatives
27 | attributes:
28 | label: Alternatives considered
29 | description: "Please provide a clear and concise description of any alternative solutions or features you've considered."
30 | validations:
31 | required: true
32 | - type: dropdown
33 | id: importance
34 | attributes:
35 | label: Importance
36 | description: How important is this feature to you?
37 | options:
38 | - nice to have
39 | - would make my life easier
40 | - i cannot use Signalize without it
41 | validations:
42 | required: true
43 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security contact information
2 |
3 | To report a security vulnerability, create an issue with the `security` tag. In case of urgency, send us a link of that issue to our e-mail `dev@signalizejs.com`.
4 |
--------------------------------------------------------------------------------
/.github/workflows/playwright.yml:
--------------------------------------------------------------------------------
1 | name: Playwright Tests
2 | on:
3 | push:
4 | branches: [ main, master ]
5 | pull_request:
6 | branches: [ main, master ]
7 | jobs:
8 | test:
9 | timeout-minutes: 60
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: 20
16 | - name: Install dependencies
17 | run: npm ci
18 | - name: Build packages
19 | run: pnpm build
20 | - name: Install Playwright Browsers
21 | run: npx playwright install --with-deps
22 | - name: Run Playwright tests
23 | run: npx playwright test
24 | - uses: actions/upload-artifact@v3
25 | if: always()
26 | with:
27 | name: playwright-report
28 | path: playwright-report
29 | retention-days: 30
30 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | tests:
13 | name: Tests
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - uses: actions/setup-node@v3
20 | with:
21 | node-version: 22
22 | cache: npm
23 |
24 | - name: Versions
25 | run: node -v && npm -v
26 |
27 | - name: Install packages
28 | run: npm run repo:init
29 |
30 | - name: Eslint
31 | run: npm run eslint:check
32 |
33 | - name: Run Playwright Tests
34 | run: npm run tests:run
35 |
36 | - uses: actions/upload-artifact@v3
37 | if: ${{ failure() }}
38 | with:
39 | name: artifacts
40 | path: |
41 | dist
42 | playwright-report/
43 | retention-days: 30
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Root
2 | node_modules
3 | *.tgz
4 | .idea
5 | /.idea
6 | dist
7 | .pnpm-store
8 | /playwright-report/
9 | /blob-report/
10 | /playwright/.cache/
11 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx
2 |
3 | RUN curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
4 | apt-get install -y nodejs &&\
5 | npm i -g npm@latest &&\
6 | node -v &&\
7 | npm -v
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-present Vladimír Macháček
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ## ✨ Introduction
31 |
32 | SignalizeJS is a client-side, multipurpose, dependency-less, easily extensible, module based microframework that uses Signals and Events under the hood.
33 | - 💎 Small learning curve
34 | - 💎 Small size - Core 2 KB
35 | - 💎 ES module based - import only what you need
36 | - 💎 No dependencies
37 |
38 | ## 📦 Ecosystem
39 | Signalize imports only a small 2 KB core and you decide what to import and when.
40 | This makes the framework small, scalable and flexible.
41 |
42 | | Package | Description |
43 | | -----------------------| -----------------------------------------------------------------------------------------------------------------------|
44 | | [ajax] | A wrapper around the native JavaScript fetch API. |
45 | | [bind] | Bind signals and values to element attributes and properties. |
46 | | [component] | Create reusable web components with minimum effort. |
47 | | [dialog] | Wrapper around native JavaScript dialog functionality. |
48 | | [directives] | Attribute Directives inspired by Vue and Svelte. |
49 | | [dom/ready] | DOM ready listener. |
50 | | [dom/traverser] | Traverse DOM nodes asynchronously. |
51 | | [evaluator] | Javascript evaluator without unsafe eval. |
52 | | [event] | Add event listener to an element or listen to global events. |
53 | | [hyperscript] | Hyperscript: create HTML elements easily with reactive data and attributes. |
54 | | [intersection-observer]| Observe changes in the element intersection with an ancestor element or with the document's viewport. |
55 | | [logger] | Wrapper around console (log, info, warning, error) for sending JS log info to the server. |
56 | | [mutation-observer] | Watch for changes being made to the DOM tree of the root or selected element. |
57 | | [offset] | Get element coordinates. |
58 | | [scope] | An utility for safely attaching data to a node prototype. |
59 | | [signal] | Reactive primitive that can be watched, used to create stores, or bound to element properties and attributes. |
60 | | [sizes] | Get computed element sizes. |
61 | | [snippets] | Redraw the current DOM elements based on string input. |
62 | | [spa] | Turn any website into a Single Page Application (SPA) in a minute. |
63 | | [strings/cases] | A set of utilities for convrting strings from one case to another. |
64 | | [task] | Schedule tasks to be executed only if there is no pending user input. Used for breaking long tasks into smaller ones. |
65 | | [viewport] | Retrieve element information relative to the current viewport. |
66 | | [visibility] | Retrieve information about HTML element visibility. |
67 |
68 | [ajax]: https://signalizejs.com/docs/modules/ajax
69 | [bind]: https://signalizejs.com/docs/modules/bind
70 | [component]: https://signalizejs.com/docs/modules/component
71 | [dialog]: https://signalizejs.com/docs/modules/dialog
72 | [directives]: https://signalizejs.com/docs/modules/directives
73 | [dom/ready]: https://signalizejs.com/docs/modules/dom-ready
74 | [dom/traverser]: https://signalizejs.com/docs/modules/dom-traverser
75 | [evaluator]: https://signalizejs.com/docs/modules/evaluator
76 | [event]: https://signalizejs.com/docs/modules/event
77 | [hyperscript]: https://signalizejs.com/docs/modules/hyperscript
78 | [intersection-observer]: https://signalizejs.com/docs/modules/intersection-observer
79 | [logger]: https://signalizejs.com/docs/modules/logger
80 | [mutation-observer]: https://signalizejs.com/docs/modules/mutation-observer
81 | [offset]: https://signalizejs.com/docs/modules/offset
82 | [scope]: https://signalizejs.com/docs/modules/scope
83 | [signal]: https://signalizejs.com/docs/modules/signal
84 | [sizes]: https://signalizejs.com/docs/modules/sizes
85 | [snippets]: https://signalizejs.com/docs/modules/snippets
86 | [spa]: https://signalizejs.com/docs/modules/spa
87 | [strings/cases]: https://signalizejs.com/docs/modules/strings-cases
88 | [task]: https://signalizejs.com/docs/modules/task
89 | [viewport]: https://signalizejs.com/docs/modules/viewport
90 | [visibility]: https://signalizejs.com/docs/modules/visibility
91 |
92 | ## Browsers Compatibility
93 | - 0.5%
94 | - Not dead
95 |
96 | ## 💡 Examples, Changelog, Issues
97 | - Live examples and tutorials: [documentation](https://signalizejs.com/docs/introduction)
98 | - Changelog and release changes: [releases](https://github.com/signalizejs/signalize/releases)
99 | - Have an idea? Found a bug? Feel free to create an [issue](https://github.com/signalizejs/signalize/issues)
100 |
101 | ## 🤟 Stay In Touch
102 |
103 | - Visit Signalize website [https://signalizejs.com](https://signalizejs.com).
104 | - Follow Signalize on [Twitter](https://twitter.com/signalizejs).
105 | - Join Signalize community on [Discord](https://discord.com/invite/V82TvAVRKY).
106 |
107 | ## 👷 Contributing
108 | Please make sure to read the [Contributing Guide](https://github.com/signalizejs/signalize/blob/master/.github/CODE_OF_CONDUCT.md) before making a pull request.
109 |
110 | ## 📝 License
111 |
112 | [MIT](https://opensource.org/licenses/MIT)
113 |
114 | Copyright (c) 2024-present, Vladimír Macháček
115 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | server:
3 | container_name: signalize
4 | build: .
5 | working_dir: /usr/share/nginx/html
6 | volumes:
7 | - ./.:/usr/share/nginx/html:delegated
8 | ports:
9 | - 3000:80
10 | # Http server for tests
11 | - 4000:4000
12 | # Playwright Report
13 | - 9323:9323
14 | environment:
15 | DEV: true
16 | tty: true
17 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from 'globals';
2 | import pluginJs from '@eslint/js';
3 | import compat from 'eslint-plugin-compat'
4 |
5 | export default [
6 | { languageOptions: { globals: globals.browser } },
7 | pluginJs.configs.recommended,
8 | compat.configs['flat/recommended'],
9 | {
10 | rules: {
11 | 'quotes': ['error', 'single'],
12 | 'prefer-const': ['error'],
13 | 'indent': ['error', 'tab'],
14 | 'key-spacing': [
15 | 'error',
16 | {
17 | 'beforeColon': false,
18 | 'afterColon': true,
19 | 'mode': 'strict',
20 | },
21 | ]
22 | }
23 | }
24 |
25 | ];
26 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Index page
10 |
41 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": "true",
3 | "type": "module",
4 | "scripts": {
5 | "repo:init": "npm i -r && npm run repo:install-playwright",
6 | "repo:install-playwright": "npx playwright install --with-deps",
7 | "repo:publish:patch": "npm login --auth-type=legacy && npm version --workspaces patch && npm publish --workspaces",
8 | "eslint:check": "eslint eslint.config.mjs packages/signalizejs/src/**/*.js",
9 | "eslint:fix": "eslint eslint.config.mjs packages/signalizejs/src/**/*.js --fix",
10 | "tests:run": "npx playwright test",
11 | "tests:report": "npx playwright show-report --host 0.0.0.0"
12 | },
13 | "author": "Vladimír Macháček",
14 | "devDependencies": {
15 | "@eslint/js": "^9.4.0",
16 | "@playwright/test": "^1.40.1",
17 | "@types/node": "^20.12.12",
18 | "eslint-plugin-compat": "^5.0.0",
19 | "globals": "^15.4.0",
20 | "http-server": "^14.1"
21 | },
22 | "workspaces": [
23 | "packages/signalizejs"
24 | ],
25 | "browserslist": [
26 | "> 0.5%, not dead"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/signalizejs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # SignalizeJS
2 |
3 | All SignalizeJS releases have their changelog [published in the Releases section](https://github.com/signalizejs/signalize/releases).
4 |
--------------------------------------------------------------------------------
/packages/signalizejs/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-present Vladimír Macháček
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/signalizejs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ## ✨ Introduction
31 |
32 | SignalizeJS is a client-side, multipurpose, dependency-less, easily extensible, module based microframework that uses Signals and Events under the hood.
33 | - 💎 Small learning curve
34 | - 💎 Small size - Core 2 KB
35 | - 💎 ES module based - import only what you need
36 | - 💎 No dependencies
37 |
38 | ## 📦 Ecosystem
39 | Signalize imports only a small 2 KB core and you decide what to import and when.
40 | This makes the framework small, scalable and flexible.
41 |
42 | | Package | Description |
43 | | -----------------------| -----------------------------------------------------------------------------------------------------------------------|
44 | | [ajax] | A wrapper around the native JavaScript fetch API. |
45 | | [bind] | Bind signals and values to element attributes and properties. |
46 | | [component] | Create reusable web components with minimum effort. |
47 | | [dialog] | Wrapper around native JavaScript dialog functionality. |
48 | | [directives] | Attribute Directives inspired by Vue and Svelte. |
49 | | [dom/ready] | DOM ready listener. |
50 | | [dom/traverser] | Traverse DOM nodes asynchronously. |
51 | | [evaluator] | Javascript evaluator without unsafe eval. |
52 | | [event] | Add event listener to an element or listen to global events. |
53 | | [hyperscript] | Hyperscript: create HTML elements easily with reactive data and attributes. |
54 | | [intersection-observer]| Observe changes in the element intersection with an ancestor element or with the document's viewport. |
55 | | [logger] | Wrapper around console (log, info, warning, error) for sending JS log info to the server. |
56 | | [mutation-observer] | Watch for changes being made to the DOM tree of the root or selected element. |
57 | | [offset] | Get element coordinates. |
58 | | [scope] | An utility for safely attaching data to a node prototype. |
59 | | [signal] | Reactive primitive that can be watched, used to create stores, or bound to element properties and attributes. |
60 | | [sizes] | Get computed element sizes. |
61 | | [snippets] | Redraw the current DOM elements based on string input. |
62 | | [spa] | Turn any website into a Single Page Application (SPA) in a minute. |
63 | | [strings/cases] | A set of utilities for convrting strings from one case to another. |
64 | | [task] | Schedule tasks to be executed only if there is no pending user input. Used for breaking long tasks into smaller ones. |
65 | | [viewport] | Retrieve element information relative to the current viewport. |
66 | | [visibility] | Retrieve information about HTML element visibility. |
67 |
68 | [ajax]: https://signalizejs/docs/modules/ajax
69 | [bind]: https://signalizejs/docs/modules/bind
70 | [component]: https://signalizejs/docs/modules/component
71 | [dialog]: https://signalizejs/docs/modules/dialog
72 | [directives]: https://signalizejs/docs/modules/directives
73 | [dom/ready]: https://signalizejs/docs/modules/dom-ready
74 | [dom/traverser]: https://signalizejs/docs/modules/dom-traverser
75 | [evaluator]: https://signalizejs/docs/modules/evaluator
76 | [event]: https://signalizejs/docs/modules/event
77 | [hyperscript]: https://signalizejs/docs/modules/hyperscript
78 | [intersection-observer]: https://signalizejs/docs/modules/intersection-observer
79 | [logger]: https://signalizejs/docs/modules/logger
80 | [mutation-observer]: https://signalizejs/docs/modules/mutation-observer
81 | [offset]: https://signalizejs/docs/modules/offset
82 | [scope]: https://signalizejs/docs/modules/scope
83 | [signal]: https://signalizejs/docs/modules/signal
84 | [sizes]: https://signalizejs/docs/modules/sizes
85 | [snippets]: https://signalizejs/docs/modules/snippets
86 | [spa]: https://signalizejs/docs/modules/spa
87 | [strings/cases]: https://signalizejs/docs/modules/strings-cases
88 | [task]: https://signalizejs/docs/modules/task
89 | [viewport]: https://signalizejs/docs/modules/viewport
90 | [visibility]: https://signalizejs/docs/modules/visibility
91 |
92 | ## Browsers Compatibility
93 | - 0.5%
94 | - Not dead
95 |
96 | ## 💡 Examples, Changelog, Issues
97 | - Live examples and tutorials: [documentation](https://signalizejs.com/docs/get-started)
98 | - Changelog and release changes: [releases](https://github.com/signalizejs/signalize/releases)
99 | - Have an idea? Found a bug? Feel free to create an [issue](https://github.com/signalizejs/signalize/issues)
100 |
101 | ## 🤟 Stay In Touch
102 |
103 | - Visit Signalize website [https://signalizejs.com](https://signalizejs.com).
104 | - Follow Signalize on [Twitter](https://twitter.com/signalizejs).
105 | - Join Signalize community on [Discord](https://discord.com/invite/V82TvAVRKY).
106 |
107 | ## 👷 Contributing
108 | Please make sure to read the [Contributing Guide](https://github.com/signalizejs/signalize/blob/master/.github/CODE_OF_CONDUCT.md) before making a pull request.
109 |
110 | ## 📝 License
111 |
112 | [MIT](https://opensource.org/licenses/MIT)
113 |
114 | Copyright (c) 2024-present, Vladimír Macháček
115 |
--------------------------------------------------------------------------------
/packages/signalizejs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "signalizejs",
3 | "version": "0.1.10",
4 | "description": "A client-side JavaScript framework designed for swift web development with minimum JavaScript.",
5 | "keywords": [
6 | "signalize",
7 | "signalizejs",
8 | "reactive",
9 | "ui",
10 | "components",
11 | "performance",
12 | "front-end",
13 | "framework",
14 | "javascript",
15 | "web"
16 | ],
17 | "type": "module",
18 | "sideEffects": false,
19 | "author": "Vladimír Macháček",
20 | "files": [
21 | "types",
22 | "src",
23 | "package.json",
24 | "LICENSE",
25 | "README.md"
26 | ],
27 | "exports": {
28 | ".": {
29 | "import": "./src/Signalize.js"
30 | },
31 | "./bind": {
32 | "import": "./src/modules/bind.js"
33 | },
34 | "./component": {
35 | "import": "./src/modules/component.js"
36 | },
37 | "./dialog": {
38 | "import": "./src/modules/dialog.js"
39 | },
40 | "./directives": {
41 | "import": "./src/modules/directives.js"
42 | },
43 | "./directives/for": {
44 | "import": "./src/modules/directives/for.js"
45 | },
46 | "./directives/if": {
47 | "import": "./src/modules/directives/if.js"
48 | },
49 | "./dom/ready": {
50 | "import": "./src/modules/dom/ready.js"
51 | },
52 | "./dom/traverser": {
53 | "import": "./src/modules/dom/traverser.js"
54 | },
55 | "./evaluator": {
56 | "import": "./src/modules/evaluator.js"
57 | },
58 | "./event": {
59 | "import": "./src/modules/event.js"
60 | },
61 | "./ajax": {
62 | "import": "./src/modules/ajax.js"
63 | },
64 | "./hyperscript": {
65 | "import": "./src/modules/hyperscript.js"
66 | },
67 | "./intersection-observer": {
68 | "import": "./src/modules/intersection-observer.js"
69 | },
70 | "./visibility": {
71 | "import": "./src/modules/visibility.js"
72 | },
73 | "./logger": {
74 | "import": "./src/modules/logger.js"
75 | },
76 | "./mutation-observer": {
77 | "import": "./src/modules/mutation-observer.js"
78 | },
79 | "./offset": {
80 | "import": "./src/modules/offset.js"
81 | },
82 | "./scope": {
83 | "import": "./src/modules/scope.js"
84 | },
85 | "./signal": {
86 | "import": "./src/modules/signal.js"
87 | },
88 | "./sizes": {
89 | "import": "./src/modules/sizes.js"
90 | },
91 | "./snippets": {
92 | "import": "./src/modules/snippets.js"
93 | },
94 | "./spa": {
95 | "import": "./src/modules/spa.js"
96 | },
97 | "./strings/cases": {
98 | "import": "./src/modules/strings/cases.js"
99 | },
100 | "./task": {
101 | "import": "./src/modules/task.js"
102 | },
103 | "./viewport": {
104 | "import": "./src/modules/viewport.js"
105 | },
106 | "./types/*": "./types/*"
107 | },
108 | "types": "types/index.d.ts",
109 | "main": "src/Signalize.js",
110 | "jsdelivr": "src/Signalize.js",
111 | "unpkg": "src/Signalize.js",
112 | "module": "src/Signalize.js",
113 | "license": "MIT"
114 | }
115 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/Signalize.js:
--------------------------------------------------------------------------------
1 | export class Signalize {
2 | /**
3 | * List of internal modules in /modules,
4 | * so they can be resolved without full package name like signalizejs/something
5 | */
6 | #internalModules = [
7 | 'ajax',
8 | 'bind',
9 | 'component',
10 | 'dialog', 'dom/ready', 'dom/traverser', 'directives', 'directives/for', 'directives/if',
11 | 'evaluator', 'event',
12 | 'hyperscript',
13 | 'intersection-observer', 'visibility',
14 | 'mutation-observer',
15 | 'offset',
16 | 'scope', 'signal', 'sizes', 'snippets', 'spa', 'strings/cases',
17 | 'task',
18 | 'viewport',
19 | ];
20 | /** @type {Record>} */
21 | #currentlyResolvedModules = {};
22 | /** @type {Record|undefined}} */
23 | #importedModulesQueue = {};
24 | /** @type {Record} */
25 | #initedModules = {};
26 | /** @type {Promise|null} */
27 | #initPromise = null;
28 | #inited = false;
29 | /** @type {import('../types/Signalize').ModulesResolver} */
30 | #resolver = (moduleName) => import(moduleName);
31 | #instanceId = 'signalizejs';
32 | /** @type {import('../types/Signalize').Root} */
33 | root;
34 | /** @type {import('../types/Signalize').Globals} */
35 | globals = {};
36 | /** @type {import('../types/Signalize').Params} */
37 | params = {
38 | attributePrefix: '',
39 | attributeSeparator: '-'
40 | };
41 |
42 | /** @param {import('../types/Signalize').SignalizeConfig} options */
43 | constructor (options = {}) {
44 | this.root = options?.root ?? document;
45 |
46 | if (this.root?.__signalize === undefined) {
47 | this.root.__signalize = this;
48 |
49 | const init = async () => {
50 | this.#resolver = options.resolver ?? this.#resolver
51 | this.#instanceId = options?.instanceId ?? this.#instanceId;
52 | this.globals = options?.globals ?? this.globals;
53 | this.params = options?.params ?? this.params;
54 |
55 | this.root.__signalize = this;
56 |
57 | if (options?.modules) {
58 | await this.resolve(...options.modules, { waitOnInit: false });
59 | }
60 |
61 | this.#inited = true;
62 | };
63 |
64 | this.#initPromise = init();
65 | } else {
66 | const signalizeInstance = this.root.__signalize;
67 | signalizeInstance.globals = { ...signalizeInstance.globals, ...options?.globals ?? {} };
68 | }
69 |
70 | return this.root.__signalize;
71 | }
72 |
73 | /** @param {import('../types/Signalize').InitedCallback} [callback] */
74 | inited = async (callback) => {
75 | if (!this.#inited) {
76 | await this.#initPromise;
77 | }
78 |
79 | if (callback) {
80 | callback();
81 | }
82 | };
83 |
84 | /**
85 | * @template T
86 | * @type {import('../types/Signalize').Resolve}
87 | */
88 | resolve = async (...modules) => {
89 | const lastItem = modules[modules.length - 1];
90 | const lastItemIsConfig = !(Array.isArray(lastItem) || typeof lastItem === 'string');
91 | const resolveConfig = {
92 | waitOnInit: true,
93 | ...lastItemIsConfig ? lastItem : {}
94 | };
95 |
96 | if (lastItemIsConfig) {
97 | modules.pop();
98 | }
99 |
100 | if (resolveConfig.waitOnInit === true) {
101 | await this.inited();
102 | }
103 |
104 | /** @type {T} */
105 | let resolved = {};
106 |
107 | /**
108 | * @template T
109 | * @type {Promise>[]} */
110 | const importsPromises = [];
111 | const modulesToInit = [];
112 |
113 | for (const moduleToImport of modules) {
114 | /** @type {string} */
115 | let moduleName;
116 | /** @type {CallableFunction} */
117 | let moduleInitFunction;;
118 | let moduleConfig = {};
119 |
120 | if (typeof moduleToImport === 'string') {
121 | moduleName = moduleToImport;
122 | } else if (Array.isArray(moduleToImport) && moduleToImport.length > 1) {
123 | moduleName = moduleToImport[0];
124 | if (typeof moduleToImport[1] === 'function') {
125 | moduleInitFunction = moduleToImport[1];
126 | moduleConfig = moduleToImport[2] ?? moduleConfig;
127 | } else {
128 | moduleConfig = moduleToImport[1];
129 | }
130 | } else {
131 | throw new Error(`The "import" for "${moduleName}" method expects module to be a name or array with config [name, config]. Got ${JSON.stringify(moduleToImport)}.`);
132 | }
133 |
134 | moduleConfig = {
135 | ...this.params[moduleName] ?? {},
136 | ...moduleConfig
137 | }
138 |
139 | if (this.#internalModules.includes(moduleName)) {
140 | moduleName = `${this.#instanceId}/${moduleName}`;
141 | }
142 |
143 | modulesToInit.push({
144 | initFunction: moduleInitFunction,
145 | name: moduleName,
146 | config: moduleConfig
147 | });
148 |
149 | if (moduleInitFunction !== undefined) {
150 | if (this.#importedModulesQueue[moduleName]) {
151 | throw new Error(`Cannot initialize module "${moduleName}" twice with different init function.`);
152 | }
153 |
154 | this.#importedModulesQueue[moduleName] = {
155 | initFunction: moduleInitFunction,
156 | config: moduleConfig
157 | }
158 | }
159 | }
160 |
161 | for (const moduleToInit of modulesToInit) {
162 | const { name, config, initFunction } = moduleToInit;
163 |
164 | const configIsEmpty = Object.keys(config).length === 0;
165 |
166 | if (name in this.#initedModules && configIsEmpty) {
167 | resolved = {
168 | ...resolved,
169 | ...this.#initedModules[name]
170 | };
171 | continue;
172 | }
173 |
174 | let modulePromise;
175 |
176 | const canBeCached = !(name in this.#initedModules) && (configIsEmpty || !this.#inited);
177 |
178 | if (canBeCached && !(name in this.#currentlyResolvedModules)) {
179 | // eslint-disable-next-line no-async-promise-executor
180 | this.#currentlyResolvedModules[name] = new Promise(async (resolve, reject) => {
181 | try {
182 | let moduleFunctionality;
183 |
184 |
185 | if (initFunction !== undefined || this.#importedModulesQueue[name]?.initFunction !== undefined) {
186 | moduleFunctionality = await (initFunction ?? this.#importedModulesQueue[name]?.initFunction)(this, config);
187 | } else {
188 | const module = await this.#resolver(name);
189 | moduleFunctionality = await (module[name] ?? module.default)(this, config);
190 | }
191 |
192 | if (!(name in this.#initedModules) && (configIsEmpty || !this.#inited)) {
193 | this.#initedModules[name] = moduleFunctionality;
194 | }
195 |
196 | delete this.#currentlyResolvedModules[name];
197 | resolve(this.#initedModules[name]);
198 | } catch (e) {
199 | reject(e);
200 | }
201 | });
202 |
203 | modulePromise = this.#currentlyResolvedModules[name];
204 | } else if (canBeCached) {
205 | modulePromise = this.#currentlyResolvedModules[name];
206 | } else {
207 | throw new Error(`Module "${name}" could not be loaded.`);
208 | }
209 |
210 | importsPromises.push(modulePromise);
211 | }
212 |
213 | for (const module of await Promise.all(importsPromises)) {
214 | resolved = {
215 | ...resolved,
216 | ...module
217 | };
218 | }
219 |
220 | this.#importedModulesQueue = {};
221 | return resolved;
222 | };
223 | }
224 |
225 | export default Signalize;
226 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/ajax.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('../../types/Signalize').Module<
3 | * import('../../types/modules/ajax').AjaxModule,
4 | * import('../../types/modules/ajax').AjaxModuleConfig
5 | * >}
6 | */
7 | export default async ({ resolve }, config) => {
8 | const { dispatch } = await resolve('event');
9 |
10 | /** @type {import('../../types/modules/ajax').ajax} */
11 | const ajax = async (resource, options = {}) => {
12 | const requestOptions = { ...options };
13 | /** @type {Response|null} */
14 | let response = null;
15 | /** @type {Error | null} */
16 | let error = null;
17 |
18 | try {
19 | requestOptions.headers = {
20 | 'X-Requested-With': config?.requestedWithHeader ?? 'XMLHttpRequest',
21 | 'X-Current-Url': window.location.href,
22 | Accept: config?.acceptHeader ?? '*',
23 | ...(options.headers ?? {})
24 | }
25 |
26 | if (requestOptions?.body !== undefined) {
27 | if (requestOptions?.method === undefined) {
28 | requestOptions.method = 'POST';
29 | }
30 |
31 | if (!['string', 'number'].includes(typeof requestOptions.body) && !(requestOptions.body instanceof FormData || requestOptions.body instanceof URLSearchParams)) {
32 | requestOptions.body = JSON.stringify(requestOptions.body);
33 | requestOptions.headers['Content-Type'] = 'application/json';
34 | }
35 | }
36 |
37 | const request = fetch(resource, requestOptions);
38 |
39 | dispatch('ajax:request:start', { resource, options: requestOptions, request });
40 |
41 | response = await request;
42 |
43 | if (!response.ok) {
44 | throw new Error('Ajax error', {
45 | cause: {
46 | response
47 | }
48 | });
49 | }
50 |
51 | dispatch('ajax:request:success', { resource, options: requestOptions, request });
52 | } catch (requestError) {
53 | response = requestError.cause?.response ?? undefined;
54 | error = requestError;
55 | console.error(error);
56 | dispatch('ajax:request:error', { resource, options: requestOptions, response, error });
57 | }
58 |
59 | dispatch('ajax:request:end', { resource, options: requestOptions, response, error });
60 |
61 | return {
62 | response,
63 | error
64 | };
65 | };
66 |
67 | return { ajax };
68 | };
69 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/bind.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ resolve }) => {
3 | const { on, off, Signal, scope } = await resolve('event', 'signal', 'scope');
4 |
5 | const reactiveInputAttributes = ['value', 'checked'];
6 | const numericInputAttributes = ['range', 'number'];
7 | const textContentAttributes = ['value', 'innerHTML', 'textContent', 'innerText'];
8 | const booleanAttributes = [
9 | 'autofocus', 'autoplay',
10 | 'checked', 'controls',
11 | 'default', 'defer', 'disabled',
12 | 'formnovalidate',
13 | 'hidden',
14 | 'ismap',
15 | 'loop',
16 | 'multiple', 'muted',
17 | 'novalidate',
18 | 'open',
19 | 'readonly', 'required', 'reversed',
20 | 'scoped', 'seamless', 'selected',
21 | 'typemustmatch'
22 | ];
23 |
24 | const attributesAliases = {
25 | text: 'textContent',
26 | html: 'innerHTML'
27 | };
28 |
29 | /** @type {import('../../types/modules/bind').bind} */
30 | const bind = (element, attributes) => {
31 | const bindAttributes = () => {
32 | /** @type {CallableFunction[]} */
33 | const unwatchSignalCallbacks = [];
34 | /** @type {CallableFunction[]} */
35 | const cleanups = [];
36 | /** @type {string[]} */
37 | const bindedProps = [];
38 |
39 | // eslint-disable-next-line prefer-const
40 | for (let [attr, attrOptions] of Object.entries(attributes)) {
41 | if (bindedProps.includes(attr)) {
42 | continue;
43 | }
44 |
45 | if (attrOptions.length === 1) {
46 | attrOptions = attrOptions[0];
47 | }
48 |
49 | const attrOptionsAsArray = Array.isArray(attrOptions) ? attrOptions : [attrOptions];
50 | const isNumericInput = numericInputAttributes.includes(element.getAttribute('type') ?? '');
51 | const attributeBinder = attrOptionsAsArray.pop();
52 | const signalsToWatch = attrOptionsAsArray;
53 | const attributeBinderType = typeof attributeBinder;
54 | const attributeBinderIsSignal = attributeBinder instanceof Signal;
55 | let attributeInited = false;
56 | /** @type {any} */
57 | let previousSettedValue;
58 | let previousValue;
59 |
60 | /**
61 | * @param {string} attribute
62 | * @param {string|number} value
63 | * @returns {Promise}
64 | */
65 | const setAttribute = async (attribute, value) => {
66 | value = value instanceof Promise ? await value : value;
67 | value = value instanceof Signal ? value() : value;
68 |
69 | if (attributeInited && previousValue === value) {
70 | return;
71 | }
72 |
73 | attribute = attributesAliases[attribute] ?? attribute;
74 |
75 | if (textContentAttributes.includes(attribute)) {
76 | element[attribute] = value;
77 | } else if (booleanAttributes.includes(attribute)) {
78 | element[attribute] = !!value;
79 | } else if (attribute === 'class') {
80 | if (attributeInited) {
81 | if (previousSettedValue !== undefined) {
82 | for (const className of previousSettedValue) {
83 | element.classList.remove(className);
84 | }
85 | }
86 | }
87 |
88 | const valueToSet = value.trim().split(' ').filter((className) => className.trim().length > 0);
89 | previousSettedValue = valueToSet;
90 |
91 | for (const className of valueToSet) {
92 | element.classList.add(className);
93 | }
94 | } else {
95 | element.setAttribute(attribute, value);
96 | }
97 | previousValue = value;
98 | attributeInited = true;
99 | };
100 |
101 | if (['string', 'number'].includes(attributeBinderType)) {
102 | setAttribute(attr, attributeBinder);
103 | continue;
104 | }
105 |
106 | if (attributeBinderIsSignal) {
107 | signalsToWatch.push(attributeBinder);
108 | }
109 |
110 | /** @type {CallableFunction|null} */
111 | let getListener = null;
112 | /** @type {CallableFunction|null} */
113 | let setListener = null;
114 | /** @type {any} */
115 | let initValue = undefined;
116 | if (attributeBinderIsSignal) {
117 | getListener = () => attributeBinder();
118 | setListener = (value) => attributeBinder(value);
119 | } else {
120 | if (typeof attributeBinder?.get === 'function') {
121 | getListener = () => attributeBinder.get();
122 | }
123 |
124 | if (typeof attributeBinder?.set === 'function') {
125 | setListener = (value) => attributeBinder.set(value);
126 | }
127 |
128 | if (typeof attributeBinder?.value !== 'undefined') {
129 | initValue = attributeBinder?.value;
130 | }
131 |
132 | if (getListener === null) {
133 | if (typeof attributeBinder === 'function') {
134 | getListener = () => attributeBinder();
135 | } else if (signalsToWatch.length === 1) {
136 | getListener = () => signalsToWatch[0]();
137 | }
138 | }
139 |
140 | if (setListener === null && signalsToWatch.length === 1) {
141 | setListener = (value) => signalsToWatch[0](value);
142 | }
143 | }
144 |
145 | if (getListener !== null || initValue !== undefined) {
146 | const valueToSet = initValue !== undefined ? initValue : getListener();
147 | setAttribute(attr, valueToSet);
148 | }
149 |
150 | for (const signalToWatch of signalsToWatch) {
151 | unwatchSignalCallbacks.push(
152 | signalToWatch.watch(() => setAttribute(attr, getListener()))
153 | );
154 | }
155 |
156 | if (typeof setListener === 'function' && reactiveInputAttributes.includes(attr)) {
157 | const inputListener = () => {
158 | setListener(isNumericInput ? Number(element[attr].replace(',', '.')) : element[attr]);
159 | };
160 |
161 | on('input', element, inputListener, { passive: true });
162 | cleanups.push(() => off('input', element, inputListener));
163 | }
164 | }
165 |
166 | scope(element, ({ $cleanup }) => {
167 | $cleanup(() => {
168 | for (const cleanup of cleanups) {
169 | cleanup();
170 | }
171 |
172 | for (const unwatch of unwatchSignalCallbacks) {
173 | unwatch();
174 | }
175 | });
176 | });
177 | };
178 |
179 | bindAttributes();
180 | };
181 |
182 | return { bind };
183 | };
184 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/component.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('../../types/Signalize').Module<
3 | * import('../../types/modules/component').ComponentModule,
4 | * import('../../types/modules/component').ComponentModuleConfig
5 | * >}
6 | */
7 | export default async ({ resolve, params }, config) => {
8 | const { componentPrefix = '' } = config;
9 | const { attributePrefix } = params;
10 | const refAttribute = `${attributePrefix}ref`;
11 | const cloakAttribute = `${attributePrefix}cloak`;
12 |
13 | const { dispatch, scope, signal, dashCase } = await resolve('event', 'scope', 'signal', 'strings/cases');
14 |
15 | /** @type {import('../../types/modules/component').component} */
16 | const component = (name, optionsOrSetup) => {
17 | let options = optionsOrSetup;
18 | let props = {};
19 | /** @type {import('../../types/modules/component').setupCallback|undefined} */
20 | let setup;
21 |
22 | if (typeof optionsOrSetup === 'function') {
23 | options = {};
24 | setup = optionsOrSetup;
25 | } else {
26 | setup = optionsOrSetup?.setup;
27 | props = optionsOrSetup?.props ?? {};
28 | }
29 |
30 | const componentName = `${componentPrefix}${name}`;
31 | const definedElement = customElements.get(componentName);
32 |
33 | if (definedElement !== undefined) {
34 | console.warn(`Custom element "${componentName}" already defined. Skipping.`);
35 | return definedElement;
36 | }
37 |
38 | let propertyKeys = [];
39 | let propsAreArray = false;
40 | let propsAreFunction = false;
41 | let propsAreObject = false;
42 |
43 | if (Array.isArray(props)) {
44 | propertyKeys = props;
45 | propsAreArray = true;
46 | } else if (typeof props === 'function') {
47 | propertyKeys = Object.keys(props());
48 | propsAreFunction = true;
49 | } else if (typeof props === 'object') {
50 | propsAreObject = true;
51 | propertyKeys = Object.keys(props);
52 | }
53 |
54 | /** @type {import('../../types/modules/component').$propsAliases} */
55 | const attributesPropertiesMap = {};
56 |
57 | for (const propertyName of propertyKeys) {
58 | attributesPropertiesMap[dashCase(propertyName)] = propertyName;
59 | }
60 |
61 | const observableAttributes = Object.keys(attributesPropertiesMap);
62 |
63 | class Component extends HTMLElement {
64 | /**
65 | * @type {string[]}
66 | */
67 | static observedAttributes = observableAttributes;
68 |
69 | /**
70 | * @type {Promise}
71 | */
72 | #constructPromise;
73 | #inited = false;
74 | /** @type {import('../../types/modules/component').ComponentScope} */
75 | #scope;
76 | /** @type {import('../../types/modules/component').LifeCycleListeners} */
77 | #connected = [];
78 | /** @type {import('../../types/modules/component').LifeCycleListeners} */
79 | #disconnected = [];
80 | /** @type {import('../../types/modules/component').LifeCycleListeners} */
81 | #adopted = [];
82 |
83 | constructor() {
84 | super();
85 | this.#constructPromise = this.#setup();
86 | }
87 |
88 | /** @return {Promise} */
89 | async #setup() {
90 | /** @type {HTMLElement|ShadowRoot} */
91 | let root = this;
92 |
93 | if (options?.shadow) {
94 | root = this.attachShadow({
95 | ...options?.shadow
96 | });
97 | }
98 |
99 | this.#scope = scope(root, (/** @type {import('../../types/modules/component.d.ts').ComponentScope} */ node) => {
100 | node.$propsAliases = attributesPropertiesMap;
101 | node.$props = {};
102 |
103 | /** @type {Record} */
104 | let properties = {};
105 |
106 | if (propsAreArray) {
107 | for (const propertyName of props) {
108 | properties[propertyName] = undefined;
109 | }
110 | } else if (propsAreFunction) {
111 | properties = props();
112 | } else if (propsAreObject) {
113 | properties = structuredClone(props);
114 | }
115 |
116 | for (const [key, value] of Object.entries(properties)) {
117 | node.$props[key] = signal(value);
118 | node.$data[key] = node.$props[key];
119 | }
120 |
121 | node.$refs = new Proxy({}, {
122 | get: (target, key) => {
123 | const refs = [...this.#scope.$el.querySelectorAll(`[${refAttribute}=${key}]`)].filter((element) => {
124 | const checkParentElement = (el) => {
125 | const parentElement = el.parentNode;
126 | if (parentElement === this.#scope.$el) {
127 | return true;
128 | }
129 |
130 | if (parentElement.tagName.toLowerCase().includes('-')) {
131 | return false;
132 | }
133 |
134 | return checkParentElement(parentElement);
135 | };
136 |
137 | return checkParentElement(element);
138 | });
139 |
140 | if (refs.length === 0) {
141 | return null;
142 | }
143 |
144 | return refs.length === 1 ? refs[0] : refs;
145 | }
146 | });
147 | });
148 |
149 | // Sometime, it's easier to wait on child components to be defined.
150 | // It can save a lot of boilerplate code.
151 | const dependencies = [];
152 |
153 | for (const componentDependency of options?.components ?? []) {
154 | if (!customElements.get(componentDependency)) {
155 | dependencies.push(new Promise(async (resolve) => {
156 | await customElements.whenDefined(componentDependency);
157 | resolve(true);
158 | }));
159 | }
160 | }
161 |
162 | await Promise.all(dependencies);
163 |
164 | for (const attr of this.#scope.$el.attributes) {
165 | this.attributeChangedCallback(attr.name, undefined, this.#scope.$el.getAttribute(attr.name));
166 | }
167 |
168 | await dispatch('component:beforeSetup', this.#scope, { target: this.#scope.$el, bubbles: true });
169 |
170 | if (setup !== undefined) {
171 | const data = await setup.call(undefined, {
172 | ...this.#scope,
173 | $connected: (listener) => this.#connected.push(listener),
174 | $disconnected: (listener) => this.#disconnected.push(listener),
175 | $adopted: (listener) => this.#adopted.push(listener),
176 | });
177 |
178 | for (const [key, value] of Object.entries(data ?? {})) {
179 | this.#scope.$data[key] = value;
180 | }
181 | }
182 |
183 | this.#scope._setuped = true;
184 | dispatch('component:setuped', this.#scope, { target: this.#scope.$el, bubbles: true });
185 | }
186 |
187 | /**
188 | * @param {string} name
189 | * @param {string|undefined} oldValue
190 | * @param {string} newValue
191 | */
192 | attributeChangedCallback(name, oldValue, newValue) {
193 | if (!observableAttributes.includes(name)) {
194 | return;
195 | }
196 |
197 | const valueTypeMap = {
198 | null: null,
199 | undefined: undefined,
200 | false: false,
201 | true: true
202 | };
203 |
204 | const currentProperty = this.#scope.$props[attributesPropertiesMap[name]];
205 | /** @type {string|number|boolean|undefined} */
206 | let valueToSet = Number.isNaN(parseFloat(currentProperty())) ? newValue : parseFloat(newValue);
207 |
208 | if (typeof currentProperty() === 'boolean') {
209 | if (valueToSet.length > 0 && valueToSet in valueTypeMap) {
210 | valueToSet = valueTypeMap[valueToSet];
211 | } else if (valueToSet.length === 0) {
212 | valueToSet = this.#scope.$el.hasAttribute(name);
213 | }
214 | }
215 |
216 | if (valueToSet !== currentProperty()) {
217 | const newPropertyValue = String(valueToSet) in valueTypeMap ? valueTypeMap[valueToSet] : valueToSet;
218 | if (this.#inited) {
219 | currentProperty(newPropertyValue);
220 | } else {
221 | currentProperty.value = newPropertyValue;
222 | }
223 | }
224 | }
225 |
226 | async connectedCallback() {
227 | await this.#constructPromise;
228 |
229 | this.#callLifeCycleListeners(this.#connected);
230 | this.#inited = true;
231 | this.removeAttribute(cloakAttribute);
232 | dispatch('component:connected', this.#scope, { target: this.#scope.$el });
233 | }
234 |
235 | async disconnectedCallback() {
236 | this.#callLifeCycleListeners(this.#disconnected);
237 | dispatch('component:disconnected', this.#scope, { target: this.#scope.$el });
238 | }
239 |
240 | async adoptedCallback() {
241 | this.#callLifeCycleListeners(this.#adopted);
242 | dispatch('component:adopted', this.#scope, { target: this.#scope.$el });
243 | }
244 |
245 | /** @param {CallableFunction[]} lifeCycleListeners */
246 | #callLifeCycleListeners = async (lifeCycleListeners) => {
247 | /** @type {Promise[]} */
248 | const listeners = [];
249 |
250 | for (const listener of lifeCycleListeners) {
251 | listeners.push(listener());
252 | }
253 |
254 | await Promise.all(listeners);
255 | }
256 | }
257 |
258 | customElements.define(componentName, Component);
259 | return Component;
260 | };
261 |
262 | return { component };
263 | };
264 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/dialog.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ resolve, root, params }) => {
3 | const { attributePrefix, attributeSeparator } = params;
4 |
5 | const { dispatch, on, off } = await resolve('event', 'dom/ready');
6 |
7 | const dialogAttribute = `${attributePrefix}dialog`;
8 | const dialogModelessAttribute = `${dialogAttribute}${attributeSeparator}modeless`;
9 | const dialogClosableAttribute = `${dialogAttribute}${attributeSeparator}closable`;
10 | const dialogCloseButtonAttribute = `${dialogAttribute}${attributeSeparator}close`;
11 | const dialogOpenButtonAttribute = `${dialogAttribute}${attributeSeparator}open`;
12 |
13 | /**
14 | * @param {MouseEvent} event
15 | */
16 | const closeOnBackDropClickListener = (event) => {
17 | const { target, clientX, clientY } = event;
18 | const rect = target?.getBoundingClientRect();
19 |
20 | if (target && (rect.left > clientX ||
21 | rect.right < clientX ||
22 | rect.top > clientY ||
23 | rect.bottom < clientY) &&
24 | target.tagName.toLowerCase() === 'dialog'
25 | ) {
26 | closeDialog(target);
27 | off('click', target, closeOnBackDropClickListener);
28 | }
29 | };
30 |
31 | /** @type {import('../../types/modules/dialog').getDialog} */
32 | const getDialog = (id) => root.querySelector(`[${dialogAttribute}=${id}]`);
33 |
34 | /** @type {import('../../types/modules/dialog').openDialog} */
35 | const openDialog = (dialogOrId, options = {}) => {
36 | const dialog = typeof dialogOrId === 'string' ? getDialog(dialogOrId) : dialogOrId;
37 |
38 | if (dialog === null) {
39 | throw new Error(`Dialog "${dialogOrId}" not found.`);
40 | }
41 |
42 | // eslint-disable-next-line prefer-const
43 | let { modelessly = false, closable = true } = options;
44 |
45 | if (dialog.hasAttribute(dialogModelessAttribute)) {
46 | modelessly = dialog.getAttribute(dialogModelessAttribute) === 'true';
47 | }
48 |
49 | dialog.setAttribute(dialogClosableAttribute, String(closable));
50 |
51 | modelessly ? dialog.show() : dialog.showModal();
52 | const dialogId = dialog.getAttribute(dialogAttribute);
53 | if (dialogId) {
54 | window.location.hash = `#${dialogId}`;
55 | }
56 |
57 | dispatch('dialog:opened', dialog);
58 |
59 |
60 | if (closable && !modelessly && dialog.getAttribute(dialogClosableAttribute) !== 'false') {
61 | on('click', dialog, closeOnBackDropClickListener);
62 | }
63 |
64 | return dialog;
65 | };
66 |
67 | /** @type {import('../../types/modules/dialog').closeDialog} */
68 | const closeDialog = (dialogOrId) => {
69 | const dialog = typeof dialogOrId === 'string' ? getDialog(dialogOrId) : dialogOrId;
70 |
71 | if (dialog != null) {
72 | dialog.close();
73 |
74 | if (dialog.getAttribute(dialogAttribute) === window.location.hash.substring(1)) {
75 | window.history.replaceState(
76 | null, '', window.location.href.substring(0, window.location.href.indexOf('#'))
77 | );
78 | }
79 |
80 | off('click', dialog, closeOnBackDropClickListener);
81 | dispatch('dialog:closed', dialog);
82 | document.body.style = 'overflow:initial!important';
83 | }
84 |
85 | return dialog;
86 | };
87 |
88 |
89 | const openDialogByUrlHash = () => {
90 | const id = window.location.hash.substring(1);
91 |
92 | if (id.length === 0 || !/^#[-\w.:]+$/.test(id)) {
93 | return;
94 | }
95 |
96 | openDialog(id);
97 | };
98 |
99 |
100 | on('click', `[${dialogCloseButtonAttribute}]`, (/** @type {MouseEvent} */ event) => {
101 | event.preventDefault();
102 | const { target } = event;
103 | const dialogId = target.getAttribute(dialogCloseButtonAttribute);
104 | const dialog = dialogId.trim().length === 0 ? target.closest('dialog') : dialogId;
105 |
106 | if (dialog !== null) {
107 | closeDialog(dialog);
108 | }
109 | });
110 |
111 | on('click', `[${dialogOpenButtonAttribute}]`, (/** @type {MouseEvent} */ event) => {
112 | event.preventDefault();
113 | const { target } = event;
114 | const dialog = getDialog(target.getAttribute(dialogOpenButtonAttribute));
115 |
116 | if (dialog != null) {
117 | openDialog(dialog);
118 | }
119 | });
120 |
121 | on('keydown', (event) => {
122 | if (event.key.toLowerCase() === 'escape'
123 | && document.querySelector(`dialog[open][${dialogClosableAttribute}="false"]`)
124 | ) {
125 | event.preventDefault();
126 | }
127 | });
128 |
129 | on('dom:ready', () => {
130 | on('locationchange', window, openDialogByUrlHash);
131 | openDialogByUrlHash();
132 | });
133 |
134 | return {
135 | closeDialog,
136 | getDialog,
137 | openDialog,
138 | };
139 |
140 | };
141 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/directives.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('../../types/Signalize').Module<
3 | * import('../../types/modules/directives').DirectivesModule,
4 | * import('../../types/modules/directives').DirectivesModuleConfig
5 | * >}
6 | */
7 | export default async ($, config) => {
8 | const { resolve, params } = $;
9 | const { attributePrefix, attributeSeparator } = params;
10 |
11 | const { on, scope, traverseDom, evaluate, bind, Signal } = await resolve(
12 | 'bind',
13 | 'dom/traverser',
14 | 'event', 'evaluator',
15 | 'scope', 'signal',
16 | );
17 |
18 | /** @type {Record}
29 | */
30 | const processElement = async (options) => {
31 | const element = options.element;
32 | let elementScope = scope(element);
33 |
34 | const compiledDirectives = elementScope?.$directives ?? new Map();
35 |
36 | let directivesQueue = [...options.$directives ?? Object.keys(directivesRegister)];
37 | const customDirectivesOrder = element.getAttribute(orderAttribute) ?? '';
38 |
39 | if (customDirectivesOrder.length > 0) {
40 | directivesQueue = [...new Set([
41 | ...customDirectivesOrder.split(',').map((item) => item.trim()).filter((item) => item.length > 0),
42 | ...directivesQueue
43 | ])];
44 | }
45 |
46 | directivesQueue = directivesQueue.filter((item) => !compiledDirectives.has(item));
47 | let countdown = element.attributes.length;
48 |
49 | if (elementScope.$directives === undefined) {
50 | while (directivesQueue.length && countdown) {
51 | const directiveName = directivesQueue.shift();
52 | const matcher = directivesRegister[directiveName]?.matcher;
53 |
54 | for (const attribute of element.attributes) {
55 | if (scope(element).$processedDirectiveAttributes?.includes(attribute.name)) {
56 | continue;
57 | }
58 |
59 | const matcherReturn = matcher({ element, attribute });
60 |
61 | if (matcherReturn === undefined) {
62 | continue;
63 | }
64 |
65 | const matches = new RegExp(`^${matcherReturn.source}$`).exec(attribute.name);
66 |
67 | if (matches === null) {
68 | continue;
69 | }
70 |
71 | elementScope = scope(element, (node) => {
72 | if (node?.$directives === undefined) {
73 | node.$processedDirectiveAttributes = [];
74 | node.$directives = new Map();
75 | }
76 | });
77 |
78 | countdown--;
79 |
80 | elementScope.$processedDirectiveAttributes.push(attribute.name);
81 |
82 | elementScope.$directives.set(
83 | directiveName,
84 | [
85 | ...elementScope.$directives.get(directiveName) ?? [],
86 | ({ elementScope }) => {
87 | const result = directivesRegister[directiveName].callback({
88 | scope: elementScope,
89 | matches,
90 | attribute
91 | });
92 |
93 | return result;
94 | }
95 | ]
96 | );
97 | }
98 | }
99 | }
100 |
101 | const directivesToRun = [...elementScope?.$directives?.keys() ?? []];
102 |
103 | /**
104 | * @param {string} name
105 | * @returns {Promise}
106 | */
107 | const runDirective = async (name) => {
108 | const promises = [];
109 |
110 | for (const directiveFunction of elementScope.$directives.get(name)) {
111 | promises.push(directiveFunction({ elementScope }));
112 | }
113 |
114 | await Promise.all(promises);
115 | };
116 |
117 | while (directivesToRun.length) {
118 | await runDirective(directivesToRun.shift());
119 | }
120 |
121 | return element;
122 | };
123 |
124 | /**
125 | * @param {Element} element
126 | * @returns
127 | */
128 | const isElementWebComponent = (element) => element.tagName.includes('-');
129 |
130 | /** @type {import('../../types/modules/directives').ProcessDirectives} */
131 | const processDirectives = async (options) => {
132 | const root = options.root;
133 | let directives = options?.directives;
134 | directives = directives ?? Object.keys(directivesRegister);
135 |
136 | const rootScope = scope(root);
137 |
138 | await traverseDom(
139 | root,
140 | async (node) => {
141 | const nodeIsRoot = node === root;
142 | if (node?.closest(`[${ignoreAttribute}]`)) {
143 | return false;
144 | }
145 |
146 | const isNestedWebComponent = isElementWebComponent(node);
147 |
148 | if (!nodeIsRoot) {
149 | scope(node, (elScope) => {
150 | if (isNestedWebComponent) {
151 | elScope._parentComponent = root;
152 | } else {
153 | elScope.$parentScope = rootScope;
154 | elScope.$data = rootScope.$data;
155 | }
156 | });
157 | }
158 |
159 | await processElement({
160 | element: node,
161 | directives,
162 | });
163 |
164 | // Detect, if node is custom element.
165 | // If so, then quit iteration after passing props above in process element.
166 | return isNestedWebComponent && !nodeIsRoot ? false : true;
167 | },
168 | [1]
169 | );
170 | };
171 |
172 | /** @type {import('../../types/modules/directives').directive} */
173 | const directive = (name, { matcher, callback }) => {
174 | if (name in directivesRegister) {
175 | throw new Error(`Directive "${name}" already defined.`);
176 | }
177 |
178 | directivesRegister[name] = {
179 | callback,
180 | matcher: typeof matcher === 'function' ? matcher : () => matcher
181 | };
182 | };
183 |
184 | /** @type {import('../../types/modules/directives').getPrerenderedNodes} */
185 | const getPrerenderedNodes = (element) => {
186 | const renderedNodes = [];
187 | let renderedTemplateSibling = element.nextSibling;
188 | let renderedTemplateOpenned = false;
189 |
190 | while (renderedTemplateSibling) {
191 | if (!renderedTemplateOpenned
192 | && renderedTemplateSibling.nodeType === Node.TEXT_NODE
193 | && renderedTemplateSibling.textContent.trim().length > 0
194 | ) {
195 | break;
196 | }
197 |
198 | if (renderedTemplateSibling.nodeType === Node.COMMENT_NODE) {
199 | const content = renderedTemplateSibling.textContent?.trim();
200 | if (content === renderedTemplateStartComment) {
201 | renderedNodes.push(renderedTemplateSibling);
202 | renderedTemplateOpenned = true;
203 | renderedTemplateSibling = renderedTemplateSibling.nextSibling;
204 | continue;
205 | } else if (content === renderedTemplateEndComment) {
206 | renderedNodes.push(renderedTemplateSibling);
207 | renderedTemplateOpenned = false;
208 | break;
209 | }
210 | }
211 |
212 | if (renderedTemplateOpenned) {
213 | renderedNodes.push(renderedTemplateSibling);
214 | }
215 |
216 | renderedTemplateSibling = renderedTemplateSibling.nextSibling;
217 | }
218 |
219 | return renderedNodes;
220 | };
221 |
222 | directive('bind', {
223 | matcher: ({ element, attribute }) => {
224 | if ([':for', ':if'].includes(attribute.name) && element.tagName.toLowerCase() === 'template') {
225 | return;
226 | }
227 |
228 | return new RegExp(`(?::|${attributePrefix}bind${attributeSeparator})([\\S-]+)|(\\{([^{}]+)\\})`);
229 | },
230 | callback: async (data) => {
231 | const { matches, attribute } = data;
232 | const elementScope = data.scope;
233 | const { $el } = elementScope;
234 | const isShorthand = attribute.name.startsWith('{');
235 | const attributeValue = isShorthand ? matches[3] : attribute.value;
236 | const attributeName = isShorthand ? matches[3] : matches[1];
237 | const isProperty = elementScope?.$props?.[attributeName] !== undefined;
238 | /** @type {Signal[]} */
239 | let trackedSignals = [];
240 |
241 | const get = (trackSignals) => {
242 | const { result, detectedSignals } = evaluate(
243 | attributeValue,
244 | {
245 | $el,
246 | ...isProperty && elementScope._parentComponent !== undefined ? scope(elementScope._parentComponent) : elementScope
247 | },
248 | trackSignals
249 | );
250 |
251 | if (trackSignals) {
252 | trackedSignals = detectedSignals;
253 | }
254 |
255 | return result;
256 | };
257 |
258 | const value = get(true);
259 |
260 | if (elementScope?.$props?.[attributeName] === undefined) {
261 | bind($el, {
262 | [attributeName]: [
263 | ...trackedSignals,
264 | {
265 | get, value,
266 | set: (value) => {
267 | trackedSignals[trackedSignals.length - 1]?.(value)
268 | }
269 | }
270 | ]
271 | });
272 | return;
273 | }
274 |
275 | if (elementScope._parentComponent === undefined) {
276 | return;
277 | }
278 |
279 | const valueIsSignal = value instanceof Signal;
280 |
281 | if (!valueIsSignal) {
282 | elementScope?.$props?.[attributeName](value);
283 | return;
284 | }
285 |
286 | let setting = false;
287 |
288 | value.watch(({ newValue }) => {
289 | if (setting) {
290 | return;
291 | }
292 |
293 | setting = true;
294 | elementScope?.$props?.[attributeName](newValue);
295 | setting = false;
296 | }, { immediate: true });
297 |
298 | elementScope?.$props?.[attributeName].watch(({ newValue }) => {
299 | if (setting) {
300 | return;
301 | }
302 |
303 | setting = true;
304 | value(newValue);
305 | setting = false;
306 | });
307 | }
308 | });
309 |
310 | directive('on', {
311 | matcher: new RegExp(`(?:\\@|${attributePrefix}on${attributeSeparator})(\\S+)`),
312 | callback: async ({ matches, scope, attribute }) => {
313 | on(matches[1], scope.$el, async (event) => {
314 | const { result } = evaluate(attribute.value, {
315 | ...scope,
316 | $event: event
317 | });
318 | if (typeof result === 'function') {
319 | result(event);
320 | }
321 | });
322 | }
323 | });
324 |
325 | directive('for', {
326 | matcher: ({ element }) => {
327 | if (element.tagName.toLocaleLowerCase() !== 'template') {
328 | return;
329 | }
330 |
331 | return new RegExp(`(?::|${attributePrefix})for`);
332 | },
333 | callback: async (data) => {
334 | const { forDirective } = await resolve('directives/for');
335 | await forDirective(data);
336 | }
337 | });
338 |
339 | directive('if', {
340 | matcher: ({ element }) => {
341 | if (element.tagName.toLowerCase() !== 'template') {
342 | return;
343 | }
344 |
345 | return new RegExp(`(?::|${attributePrefix})if`);
346 | },
347 | callback: async (data) => {
348 | const { ifDirective } = await resolve('directives/if');
349 | await ifDirective(data);
350 | }
351 | });
352 |
353 | on('component:setuped', async (event) => await processDirectives({ root: event.detail.$el }));
354 |
355 | return {
356 | getPrerenderedNodes,
357 | processDirectives,
358 | directive
359 | };
360 | };
361 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/directives/for.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../../types/Signalize').Module} */
2 | export default async ({ resolve, params }) => {
3 | const { attributePrefix } = params;
4 | /** @type {{
5 | * getPrerenderedNodes: import('../../../types/modules/directives').getPrerenderedNodes,
6 | * processDirectives: import('../../../types/modules/directives').processDirectives,
7 | * evaluate: import('../../../types/modules/evaluator').evaluate,
8 | * signal: import('../../../types/modules/signal').signal,
9 | * Signal: import('../../../types/modules/signal').Signal,
10 | * scope: import('../../../types/modules/scope').scope,
11 | * }} */
12 | const resolved = await resolve('directives', 'evaluator', 'scope', 'signal');
13 | const { getPrerenderedNodes, evaluate, processDirectives, signal, Signal } = resolved;
14 | const _scope = resolved.scope;
15 |
16 | const forDirective = async ({ scope, attribute }) => {
17 | const { $el, $parentScope } = scope;
18 |
19 | if ($el.tagName.toLowerCase() !== 'template') {
20 | return;
21 | }
22 |
23 | const forLoopRe = /([\s\S]+)\s+(in|of)\s+([\s\S]+)/;
24 | const argumentsMatch = attribute.value.match(forLoopRe);
25 |
26 | if (argumentsMatch.length < 4) {
27 | throw new Error(`Invalid for loop syntax "${attribute.value}".`);
28 | }
29 |
30 | /** @type {string[]} */
31 | const newContextVariables = argumentsMatch[1].replace(/[[({})\]\s]/g, '').split(',').map((key) => key.trim());
32 | let currentState = getPrerenderedNodes($el);
33 | /** @type {Record} */
34 | let currentStateKeys = {};
35 | /** @type {Element[]} */
36 | let newState = [];
37 | const prerendered = currentState.length > 0;
38 |
39 | const reduceState = () => {
40 | let i = currentState.length;
41 |
42 | while (i--) {
43 | let item = currentState[i];
44 |
45 | if (newState.includes(item)) {
46 | continue;
47 | }
48 |
49 | item.remove();
50 | }
51 | };
52 |
53 | /** @param {Element} element */
54 | const evaluateKey = (element) => {
55 | let generated = element.getAttribute(`${attributePrefix}key`);
56 |
57 | if (generated) {
58 | return generated;
59 | }
60 |
61 | let key = null;
62 | const keyFnString = element.getAttribute(`:${attributePrefix}key`);
63 |
64 | if (keyFnString) {
65 | const { result } = evaluate(keyFnString,_scope(element) ?? {});
66 | key = result;
67 | }
68 |
69 | return key;
70 | };
71 |
72 | let inited = false;
73 | /** @type {import('../../../types/modules/signal').Signal[]} */
74 | let loopSignalsToWatch = [];
75 |
76 | /**
77 | * @returns {void}
78 | */
79 | const process = () => {
80 | // eslint-disable-next-line prefer-const
81 | let { result, detectedSignals } = evaluate(argumentsMatch[3], scope, !inited);
82 |
83 | result = result instanceof Signal ? result() : result;
84 |
85 | if (!inited) {
86 | loopSignalsToWatch = detectedSignals;
87 | inited = true;
88 |
89 | if (prerendered) {
90 | return;
91 | }
92 | }
93 |
94 | if (typeof result === 'number') {
95 | result = [...Array(result).keys()];
96 | }
97 |
98 | const totalCount = result.length ?? result.size;
99 | let counter = 0;
100 |
101 | const isArrayDestruct = argumentsMatch[0].trim().startsWith('[');
102 |
103 | for (const index in currentState) {
104 | const node = currentState[index];
105 |
106 | if (!(node instanceof Element)) {
107 | continue;
108 | }
109 |
110 | const key = node.getAttribute('key');
111 |
112 | if (key) {
113 | currentStateKeys[key] = currentState[index];
114 | }
115 | }
116 |
117 | /**
118 | *
119 | * @param {any} context
120 | * @param {number} counter
121 | * @returns {void}
122 | */
123 | const iterate = (context, counter) => {
124 | const iterator = signal({
125 | counter,
126 | first: counter === 0,
127 | last: counter === totalCount - 1,
128 | odd: counter % 2 !== 0,
129 | even: counter % 2 === 0
130 | });
131 | /** @type {Record} */
132 | const destruct = {};
133 |
134 | if (newContextVariables.length > 1) {
135 | if (isArrayDestruct) {
136 | for (const key of Object.keys(context)) {
137 | destruct[newContextVariables[key]] = context[key];
138 | }
139 | } else {
140 | for (const key of newContextVariables) {
141 | destruct[key] = context[key];
142 | }
143 | }
144 | } else {
145 | destruct[newContextVariables] = context;
146 | }
147 |
148 |
149 | const templateFragment = [...$el.cloneNode(true).content.children];
150 |
151 | while (templateFragment.length > 0) {
152 | const fragment = templateFragment.shift();
153 |
154 | const fragmentScope = _scope(fragment, (elScope) => {
155 | elScope.$parentScope = $parentScope;
156 | elScope.$data = {
157 | ...scope.$data,
158 | ...destruct,
159 | iterator
160 | };
161 | elScope.$template = $el;
162 | });
163 |
164 | const fragmentKey = evaluateKey(fragment);
165 | fragment.removeAttribute('key');
166 |
167 | if (fragmentKey && fragmentKey in currentStateKeys) {
168 | newState.push(currentStateKeys[fragmentKey]);
169 | return;
170 | }
171 |
172 | for (const child of fragment.children) {
173 | _scope(child, (childScope) => {
174 | childScope.$data = fragmentScope.$data;
175 | childScope.$parentScope = $parentScope;
176 | });
177 | }
178 |
179 | void processDirectives({ root: fragment });
180 | newState.push(fragment);
181 | }
182 | };
183 |
184 | if (argumentsMatch[2] === 'in') {
185 | for (const stackItem in result) {
186 | iterate(stackItem, counter++);
187 | }
188 | } else {
189 | for (let stackItem of result) {
190 | iterate(stackItem, counter++);
191 | }
192 | }
193 |
194 | let insertPoint = $el;
195 |
196 | reduceState();
197 |
198 | for (const index in newState) {
199 | const fragment = newState[index];
200 | const currentInsertPoint = insertPoint;
201 | insertPoint = fragment;
202 |
203 | if (currentState[index] === fragment) {
204 | continue;
205 | }
206 |
207 | currentInsertPoint.after(fragment);
208 | }
209 |
210 | currentStateKeys = {};
211 | currentState = newState;
212 | newState = [];
213 | };
214 |
215 | process();
216 |
217 | /** @type {CallableFunction[]} */
218 | const unwatchSignalCallbacks = [];
219 | for (const signalToWatch of loopSignalsToWatch) {
220 | unwatchSignalCallbacks.push(signalToWatch.watch(process));
221 | }
222 |
223 | _scope($el, (elScope) => {
224 | elScope.$cleanup(() => {
225 | reduceState();
226 | while (unwatchSignalCallbacks.length > 0) {
227 | unwatchSignalCallbacks.shift()();
228 | }
229 | });
230 | });
231 | };
232 |
233 | return { forDirective };
234 | };
235 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/directives/if.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../../types/Signalize').Module} */
2 | export default async ({ resolve }) => {
3 | const resolved = await resolve('directives', 'evaluator', 'scope');
4 | const { getPrerenderedNodes, evaluate, processDirectives } = resolved;
5 | const _scope = resolved.scope;
6 |
7 | const ifDirective = async ({ scope, attribute }) => {
8 | const { $el } = scope;
9 | const nextSiblingScope = _scope($el.nextSibling);
10 | let rendered = nextSiblingScope?.template === $el;
11 | let previousResult = rendered;
12 | let prerendered = true;
13 | let renderedNodes = [];
14 |
15 | if (rendered === false) {
16 | renderedNodes = getPrerenderedNodes($el);
17 | if (renderedNodes.length) {
18 | rendered = true;
19 | prerendered = true;
20 | }
21 | }
22 | let inited = false;
23 | let ifSignalsToWatch = [];
24 |
25 | /**
26 | * @returns {Promise}
27 | */
28 | const render = async () => {
29 | let { result, detectedSignals } = evaluate(attribute.value, scope, !inited);
30 | result = typeof result === 'function' ? result() : result;
31 |
32 | if (!inited) {
33 | ifSignalsToWatch = detectedSignals;
34 | inited = true;
35 |
36 | if (rendered) {
37 | return;
38 | }
39 | }
40 |
41 | if (result === previousResult) {
42 | return;
43 | }
44 |
45 | previousResult = result;
46 |
47 | if (result !== true || prerendered) {
48 | while (renderedNodes.length > 0) {
49 | renderedNodes.pop().remove();
50 | }
51 | }
52 |
53 | if (result !== true) {
54 | rendered = false;
55 | return;
56 | }
57 |
58 | let fragment = $el.cloneNode(true).content;
59 |
60 | _scope(fragment, (fragmentScope) => {
61 | fragmentScope.$parentScope = scope;
62 | fragmentScope.$data = scope.$data;
63 | });
64 |
65 | await processDirectives({ root: fragment });
66 | renderedNodes = [...fragment.childNodes];
67 | $el.after(fragment);
68 | rendered = true;
69 | };
70 |
71 | await render();
72 |
73 | const unwatchSignalCallbacks = [];
74 |
75 | while (ifSignalsToWatch.length) {
76 | unwatchSignalCallbacks.push(ifSignalsToWatch.shift().watch(render));
77 | }
78 |
79 | scope.$cleanup(() => {
80 | while (renderedNodes.length > 0) {
81 | renderedNodes.pop().remove();
82 | }
83 | for (const unwatch of unwatchSignalCallbacks) {
84 | unwatch();
85 | }
86 | });
87 | };
88 |
89 | return { ifDirective };
90 | };
91 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/dom/ready.js:
--------------------------------------------------------------------------------
1 |
2 | /** @type {import('../../../types/Signalize').Module} */
3 | export default async ({ resolve, root }) => {
4 | /**
5 | * @type {{
6 | * customEventListener: import('../../../types/modules/event').customEventListener
7 | * }}
8 | */
9 | const { customEventListener } = await resolve('event', { waitOnInit: false });
10 |
11 | /** @type {CallableFunction[]} */
12 | const domReadyListeners = [];
13 |
14 | const callOnDomReadyListeners = () => {
15 | while (domReadyListeners.length > 0) {
16 | const listener = domReadyListeners.shift();
17 |
18 | if (typeof listener !== 'function') {
19 | throw new Error('Dom ready listener must be a function.');
20 | }
21 |
22 | listener();
23 | }
24 | };
25 |
26 | /** @type {import('../../../types/modules/dom/ready').isDomReady} */
27 | const isDomReady = () => {
28 | const documentElement = root instanceof Document ? root : root?.ownerDocument;
29 | return documentElement.readyState !== 'loading';
30 | };
31 |
32 | customEventListener('dom:ready', ({
33 | on: ({ listener }) => {
34 | if (isDomReady()) {
35 | listener();
36 | } else {
37 | domReadyListeners.push(listener);
38 | }
39 | }
40 | }));
41 |
42 | if (isDomReady()) {
43 | callOnDomReadyListeners();
44 | } else {
45 | document.addEventListener('DOMContentLoaded', callOnDomReadyListeners, { once: true });
46 | }
47 |
48 | return {
49 | isDomReady
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/dom/traverser.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../../types/Signalize').Module} */
2 | export default () => ({
3 | /** @type {import('../../../types/modules/dom/traverser').traverseDom} */
4 | traverseDom: async (root, callback, nodeTypes = []) => {
5 | /**
6 | * @param {Node} node
7 | * @returns {Promise}
8 | */
9 | const processNode = async (node) => {
10 | node = node instanceof Document ? node.documentElement : node;
11 | if (nodeTypes.includes(node.nodeType) || nodeTypes.length === 0) {
12 | const processChildren = await callback(node);
13 | if (processChildren === false) {
14 | return;
15 | }
16 | }
17 |
18 | const childPromises = [];
19 |
20 | for (const child of node.childNodes) {
21 | childPromises.push(processNode(child));
22 | }
23 |
24 | await Promise.all(childPromises);
25 | };
26 |
27 | await processNode(root);
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/evaluator.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ resolve, globals }) => {
3 | const { Signal } = await resolve('signal');
4 | const chunkKeywordMap = {
5 | undefined,
6 | true: true,
7 | false: false,
8 | null: null,
9 | Object,
10 | Boolean,
11 | Number,
12 | String,
13 | Array,
14 | console,
15 | JSON,
16 | ...globals
17 | };
18 |
19 | const quotes = ['"', '\'', '`'];
20 | /** @type {string[]} */
21 |
22 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table
23 | /** @type {Record, CallableFunction][]>} */
24 | const precedenceOperatorsMap = {
25 | 18: [
26 | // Groups
27 | ['(', ')', ({ a, chunks, getGroupChunks, index, compile }) => {
28 | const groupTokens = getGroupChunks(chunks, index, '(', ')');
29 | const groupTokensLength = groupTokens.length;
30 |
31 | // Check if it the previous argument isnt a function call
32 | // If it is, skip to the end of the group
33 | // TODO check group at the beginning of the string
34 | if (typeof a === 'function' || (a !== undefined && !operatorsKeys.includes(a))) {
35 | return groupTokensLength + 2;
36 | }
37 |
38 | return [compile([...allPrecedences], groupTokens), groupTokensLength + 1, index];
39 | }]
40 | ],
41 | 17: [
42 | ['?.', ({ a, b }) => {
43 | const chained = a?.[b];
44 | return [typeof chained === 'function' && chained.prototype === undefined ? chained.bind(a) : chained, 2];
45 | }],
46 | ['.', ({ a, b }) => {
47 | const chained = a[b];
48 | return [typeof chained === 'function' && chained.prototype === undefined ? chained.bind(a) : chained, 2];
49 | }],
50 | ['[', ']', ({ index, a, chunks, compile, getGroupChunks }) => {
51 | const args = getGroupChunks(chunks, index, '[', ']');
52 | const compiledArgs = compile([...allPrecedences], args) ?? [];
53 | return [a[compiledArgs[0]], 3];
54 | }],
55 | // Function call
56 | ['(', ')', ({ index, a, chunks, compile, getGroupChunks }) => {
57 | const args = getGroupChunks(chunks, index, '(', ')');
58 | const argsLength = args.length;
59 | const compiledArgs = compile([...allPrecedences], args) ?? [];
60 | const applyArgs = Array.isArray(compiledArgs) ? compiledArgs : [compiledArgs];
61 |
62 | if (typeof a !== 'function') {
63 | throw new Error(`"${a}" is not a function.`);
64 | }
65 |
66 | const applyResult = a(...applyArgs.flat());
67 |
68 | return [
69 | typeof applyResult === 'string' ? `\`${applyResult}\`` : applyResult,
70 | argsLength + 2
71 | ];
72 | }]
73 | ],
74 | 15: [
75 | ['++', ({ a }) => [a++, 1]],
76 | ['--', ({ a }) => [a--, 1]]
77 | ],
78 | 14: [
79 | ['++', ({ b, index }) => [++b, 1, index]],
80 | ['--', ({ b, index }) => [--b, 1, index]],
81 | ['!', ({ b, index }) => [!b, 1, index]],
82 | ['!!', ({ b, index }) => [!!b, 1, index]],
83 | ['typeof', ({ b, index }) => [typeof b, 1, index]]
84 | ],
85 | 13: [
86 | ['**', ({ a, b }) => [a ** b]],
87 | ],
88 | 12: [
89 | ['*', ({ a, b }) => [a * b]],
90 | ['/', ({ a, b }) => [a / b]],
91 | ['%', ({ a, b }) => [a % b]],
92 | ],
93 | 11: [
94 | ['+', ({ a, b }) => [a + b]],
95 | ['-', ({ a, b }) => [a - b]]
96 | ],
97 | 9: [
98 | ['<', ({ a, b }) => [a < b]],
99 | ['<=', ({ a, b }) => [a <= b]],
100 | ['>', ({ a, b }) => [a > b]],
101 | ['>=', ({ a, b }) => [a >= b]],
102 | ['in', ({ a, b }) => [a in b]],
103 | ['instanceof', ({ a, b }) => [a instanceof b]]
104 | ],
105 | 8: [
106 | ['==', ({ a, b }) => [a == b]],
107 | ['!=', ({ a, b }) => [a != b]],
108 | ['===', ({ a, b }) => [a === b]],
109 | ['!==', ({ a, b }) => [a !== b]]
110 | ],
111 | 7: [
112 | ['&', ({ a, b }) => [a & b]]
113 | ],
114 | 6: [
115 | ['^', ({ a, b }) => [a ^ b]]
116 | ],
117 | 5: [
118 | ['|', ({ a, b }) => [a | b]]
119 | ],
120 | 4: [
121 | ['&&', ({ a, b }) => [a && b]]
122 | ],
123 | 3: [
124 | ['||', ({ a, b }) => [a || b]],
125 | ['??', ({ a, b }) => [a ?? b]]
126 | ],
127 | 2: [
128 | ['?', ':', ({ a, chunks, prepareChunk }) => {
129 | const b = [];
130 | const c = [];
131 | let startIndex = 1;
132 | let colonFound = false;
133 | const chunksLength = chunks.length - 1;
134 | while (startIndex < chunksLength) {
135 | startIndex += 1;
136 | const token = chunks[startIndex];
137 | const isColon = token === ':';
138 |
139 | if (!colonFound) {
140 | colonFound = isColon;
141 | if (colonFound) {
142 | continue;
143 | }
144 | }
145 |
146 | if (!isColon && operatorsKeys.includes(token)) {
147 | break;
148 | }
149 |
150 | if (colonFound) {
151 | c.push(prepareChunk(chunks[startIndex]));
152 | } else {
153 | b.push(prepareChunk(chunks[startIndex]));
154 | }
155 | }
156 |
157 | return [a ? b.join('') : c.join(''), b.length + c.length + 2];
158 | }]
159 | ],
160 | 1: [
161 | [',', ({ a, b }) => [[...Array.isArray(a) ? a : [a], ...Array.isArray(b) ? b : [b]]]]
162 | ]
163 | };
164 |
165 | /** @type {Record} */
166 | const precedenceOperatorKeysMap = {};
167 | /** @type {Record>} */
168 | const precedenceOperatorCompilerMap = {};
169 |
170 | for (const precedence in precedenceOperatorsMap) {
171 | for (const operatorDefinition of precedenceOperatorsMap[precedence]) {
172 | const operators = Object.values(operatorDefinition);
173 | precedenceOperatorKeysMap[precedence] = [
174 | ...precedenceOperatorKeysMap[precedence] ?? [],
175 | ...operators.slice(0, -1)
176 | ];
177 | precedenceOperatorCompilerMap[precedence] = {
178 | ...precedenceOperatorCompilerMap[precedence],
179 | [operators[0]]: operators.pop()
180 | };
181 | }
182 | }
183 |
184 | const operatorsKeys = Object.values(precedenceOperatorKeysMap).flat();
185 | const operatorsRe = new RegExp(`^(${operatorsKeys
186 | .map((item) => {
187 | item = item.replace(/[|+\\/?*^.,(){}$[\]]/g, '\\$&');
188 |
189 | // When the operator is a word like "in", wrap it into the full word match to prevent matching
190 | // it in words like "increment".
191 | if (/[\w_]+/.test(item)) {
192 | item = `\b${item}\b`;
193 | }
194 |
195 | return item;
196 | })
197 | .sort((a, b) => b.length - a.length)
198 | .join('|')})`
199 | );
200 |
201 | const allPrecedences = Object.keys(precedenceOperatorsMap).sort((a, b) => b - a);
202 | const tokenizeCache = {};
203 |
204 | /** @type {import('../../types/modules/evaluator').evaluate} */
205 | const evaluate = (str, context = {}, trackSignals = false) => {
206 | const detectedSignals = new Set();
207 | const signalsUnwatchCallbacks = new Set();
208 |
209 | const tokenize = (str) => {
210 | const originalString = str;
211 |
212 | if (originalString in tokenizeCache) {
213 | return [...tokenizeCache[originalString]];
214 | }
215 |
216 | const chunks = [];
217 | let inString = false;
218 | let tokensQueue = '';
219 | let token = str[0];
220 |
221 | while (token !== undefined) {
222 | if (quotes.includes(token)) {
223 | inString = !inString;
224 | }
225 |
226 | const operatorMatch = inString ? null : str.match(operatorsRe);
227 | const operatorDetected = operatorMatch !== null;
228 |
229 | str = str.slice(operatorDetected ? operatorMatch[0].length : 1);
230 | if (operatorDetected) {
231 | if (tokensQueue.trim().length) {
232 | chunks.push(tokensQueue.trim());
233 | tokensQueue = '';
234 | }
235 |
236 | chunks.push(operatorMatch[0]);
237 | } else {
238 | tokensQueue += token;
239 | }
240 |
241 | if (str.length === 0 && tokensQueue.trim().length) {
242 | chunks.push(tokensQueue.trim());
243 | }
244 |
245 | token = str[0];
246 | }
247 |
248 | tokenizeCache[originalString] = chunks;
249 | return [...chunks];
250 | };
251 |
252 | const compile = (precedences, chunks) => {
253 | const precedence = precedences.shift();
254 |
255 | const prepareChunk = (chunk) => {
256 | let processedChunk = chunk;
257 |
258 | if (typeof chunk !== 'function') {
259 | if (quotes.includes(chunk?.[0])) {
260 | processedChunk = String(chunk.substring(1).substring(0, chunk.length - 2));
261 | } else if (chunk in chunkKeywordMap) {
262 | processedChunk = chunkKeywordMap[chunk];
263 | } else if (!Array.isArray(chunk) && /^\d+(?:\.\d+)?$/.test(chunk)) {
264 | processedChunk = parseFloat(chunk);
265 | } else if (chunk in context) {
266 | processedChunk = context[chunk];
267 | }
268 | }
269 |
270 | if (trackSignals && processedChunk instanceof Signal) {
271 | const unwatch = processedChunk.watch(() => {
272 | detectedSignals.add(processedChunk);
273 | }, { execution: 'onGet' });
274 |
275 | signalsUnwatchCallbacks.add(unwatch);
276 | }
277 |
278 | return processedChunk;
279 | };
280 |
281 | if (precedence === undefined || chunks.length === 1) {
282 | return chunks.map((item) => prepareChunk(item));
283 | }
284 |
285 | const operators = precedenceOperatorKeysMap[precedence];
286 |
287 | if (!chunks.some((chunk) => operators.includes(chunk))) {
288 | return compile(precedences, chunks);
289 | }
290 |
291 | let startIndex = 0;
292 | const chunksLength = chunks.length;
293 |
294 | /**
295 | * TODO
296 | * There will have to be an array as an argument, that will tell the parser, that the token is actually nesting token.
297 | * For example. Open token for template literals is "${" and closing "}".
298 | * But if there is something lik ${ {} } in the code, then the "{" will not be matched
299 | * as an openning/nesting bracket and the "}" will be incorrectly matched as a closing bracket
300 | * of the current template literal.
301 | *
302 | */
303 | const getGroupChunks = (chunks, cursorIndex, openToken, closeToken) => {
304 | const groupChunks = [];
305 | let closingBracesRequired = 1;
306 |
307 | while (closingBracesRequired > 0 || cursorIndex < chunks.length) {
308 | cursorIndex += 1;
309 | const token = chunks[cursorIndex];
310 |
311 | if (token === openToken) {
312 | closingBracesRequired++;
313 | }
314 |
315 | if (token === closeToken) {
316 | closingBracesRequired--;
317 | }
318 |
319 | if (closingBracesRequired === 0) {
320 | break;
321 | }
322 |
323 | groupChunks.push(chunks[cursorIndex]);
324 | }
325 |
326 | return groupChunks;
327 | };
328 |
329 | let runs = 0;
330 |
331 | while (startIndex <= chunksLength && runs <= chunksLength) {
332 | const token = chunks[startIndex];
333 |
334 | if (operators.includes(token)) {
335 | const compiler = precedenceOperatorCompilerMap[precedence]?.[token];
336 |
337 | if (compiler === undefined) {
338 | throw new Error(`Unexpected token "${token}" in ${chunks.join(' ')}`);
339 | }
340 |
341 | const result = compiler({
342 | a: prepareChunk(chunks[startIndex - 1]),
343 | b: prepareChunk(chunks[startIndex + 1]),
344 | compile,
345 | prepareChunk,
346 | getGroupChunks,
347 | index: startIndex,
348 | chunks,
349 | context
350 | });
351 |
352 | if (typeof result === 'number') {
353 | startIndex += result;
354 | } else {
355 | let resultPosition = result[2] ?? undefined;
356 | const spliceLength = result[1] ?? 2;
357 |
358 | if (resultPosition === undefined) {
359 | resultPosition = chunks[startIndex - 1] === undefined ? startIndex : startIndex - 1;
360 | }
361 |
362 | chunks[resultPosition] = result[0];
363 | chunks.splice(resultPosition + 1, spliceLength);
364 | }
365 | } else {
366 | startIndex++;
367 | }
368 |
369 | runs++;
370 | }
371 |
372 | return compile(precedences, chunks);
373 | };
374 |
375 | // There is always only one at the end of the evaluation
376 | const tokens = tokenize(str);
377 | const result = compile([...allPrecedences], tokens)[0];
378 |
379 | if (result instanceof Signal) {
380 | detectedSignals.add(result);
381 | }
382 |
383 | return { result, detectedSignals: [...detectedSignals] };
384 | };
385 |
386 | return { evaluate };
387 | };
388 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/event.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('../../types/Signalize').Module<
3 | * import('../../types/modules/event').EventModule,
4 | * import('../../types/modules/event').EventConfig
5 | * >}
6 | */
7 | export default async ({ root, resolve }) => {
8 |
9 | const { observeMutations } = await resolve('mutation-observer');
10 |
11 | /** @type {Record} */
12 | const customEventListeners = {
13 | clickoutside: {
14 | on: ({ target, listener, options }) => {
15 | document.addEventListener('click', (listenerEvent) => {
16 | /** @type {Element} */
17 | const eventTarget = listenerEvent.target;
18 |
19 | if ((typeof target === 'string' && (eventTarget.matches(target) || eventTarget.closest(target) !== null)) ||
20 | (target instanceof Element && (target === eventTarget || target.contains(eventTarget)))
21 | ) {
22 | return;
23 | }
24 |
25 | const targetIsString = typeof target === 'string';
26 | if (eventTarget !== target && (!targetIsString || (targetIsString && (!eventTarget.matches(target) || eventTarget.closest(target) === null)))) {
27 | listener(listenerEvent);
28 | }
29 | }, options);
30 | },
31 | off: ({ listener }) => {
32 | document.removeEventListener('click', listener);
33 | }
34 | },
35 | remove: {
36 | on: ({ target, listener }) => {
37 | /** @type {CallableFunction} */
38 | const unobserve = observeMutations(({ removedNodes }) => {
39 | if (removedNodes.includes(target)) {
40 | listener();
41 | unobserve();
42 | }
43 | });
44 | }
45 | }
46 | };
47 |
48 | /**
49 | * @param {Element|string|Array|NodeList} target
50 | * @param {boolean} container
51 | * @returns {Element[]}
52 | */
53 | const selectorToIterable = (target, container) => {
54 | let elements = [];
55 |
56 | if (typeof target === 'string') {
57 | for (const selector of target.split(',')) {
58 | elements = [
59 | ...elements,
60 | ...(container ?? root).querySelectorAll(selector)
61 | ];
62 | }
63 | } else {
64 | const targetIsDocument = target instanceof Document;
65 | if (target instanceof Element || targetIsDocument || target instanceof Window) {
66 | elements = [target];
67 | } else {
68 | elements = target instanceof Array || target instanceof NodeList ? [...target] : [target];
69 | }
70 | }
71 |
72 | return elements.filter((element) => element !== null);
73 | };
74 |
75 | /** @type {import('../../types/modules/event').on} */
76 | const on = (events, targetOrCallback, callbackOrOptions, options) => {
77 | let targetOrSelector;
78 | /** @type {CallableFunction} */
79 | let listener;
80 | options = typeof callbackOrOptions === 'function' ? options : callbackOrOptions;
81 |
82 | if (typeof targetOrCallback === 'function') {
83 | targetOrSelector = root;
84 | listener = targetOrCallback;
85 | } else {
86 | targetOrSelector = targetOrCallback;
87 | listener = callbackOrOptions;
88 | }
89 |
90 | /** @param {Element|Document} target */
91 | const attachListeners = (target) => {
92 | for (const event of events.split(' ').map((event) => event.trim())) {
93 | /** @type {CustomEventListenerArgs} */
94 | const listenerData = { event, target, listener, options };
95 |
96 | if (event in customEventListeners) {
97 | if (options?.once === true) {
98 | listenerData.listener = (...args) => {
99 | listener.apply(undefined, args);
100 | customEventListeners[event]?.off(listenerData);
101 | };
102 | }
103 | customEventListeners[event].on(listenerData);
104 | continue;
105 | }
106 |
107 | target.removeEventListener(event, listener);
108 | target.addEventListener(event, listener, options);
109 | }
110 | };
111 |
112 | const offCallback = () => {
113 | return off(events, targetOrSelector, listener, options)
114 | };
115 |
116 | if (typeof targetOrSelector !== 'string') {
117 | attachListeners(targetOrSelector);
118 | return offCallback;
119 | }
120 |
121 | for (const target of selectorToIterable(targetOrSelector, options?.container)) {
122 | attachListeners(target);
123 | }
124 |
125 | observeMutations(({ addedNodes }) => {
126 | for (const addedNode of addedNodes) {
127 | const selectors = targetOrSelector.split(',');
128 | while(selectors.length > 0) {
129 | const selector = selectors.pop();
130 |
131 | for (const element of [
132 | ...addedNode.matches(selector) ? [addedNode] : [],
133 | ...addedNode.querySelectorAll(selector)
134 | ]) {
135 | attachListeners(element);
136 | }
137 | }
138 | }
139 | });
140 |
141 | return offCallback;
142 | };
143 |
144 | /** @type {import('../../types/modules/event').customEventListener} */
145 | const customEventListener = (eventName, configOrHandler) => {
146 | customEventListeners[eventName] = typeof configOrHandler === 'function'
147 | ? { on: configOrHandler }
148 | : configOrHandler;
149 | };
150 |
151 | /** @type {import('../../types/modules/event').off} */
152 | const off = (events, element, listener, options = {}) => {
153 | const elements = selectorToIterable(element);
154 |
155 | for (const event of events.split(' ')) {
156 | if (event in customEventListeners) {
157 | customEventListeners[event]?.off({ event, target: element, listener, options });
158 | continue;
159 | }
160 |
161 | for (const element of elements) {
162 | element.removeEventListener(event, listener, options);
163 | }
164 | }
165 | };
166 |
167 | /** @type {import('../../types/modules/event').customEvent} */
168 | const customEvent = (eventName, eventData, options) => new window.CustomEvent(eventName, {
169 | detail: eventData,
170 | cancelable: options?.cancelable ?? false,
171 | bubbles: options?.bubbles ?? false
172 | });
173 |
174 | /** @type {import('../../types/modules/event').dispatch} */
175 | const dispatch = (eventName, eventData, options) => {
176 | const event = customEvent(eventName, eventData, options);
177 |
178 | if (typeof customEventListeners[eventName]?.dispatch === 'function') {
179 | customEventListeners[eventName].dispatch(event);
180 | return false;
181 | }
182 |
183 | return (options?.target ?? root).dispatchEvent(event);
184 | };
185 |
186 | return {
187 | on,
188 | off,
189 | customEventListener,
190 | dispatch,
191 | customEvent,
192 | };
193 | };
194 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/hyperscript.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ resolve }) => {
3 |
4 | const { bind, Signal } = await resolve('bind', 'signal');
5 |
6 | /**
7 | * @template T
8 | * @type {import('../../types/modules/hyperscript').h}
9 | */
10 | const h = (tagName, ...children) => {
11 | /** @type {import('../../types/modules/hyperscript').HyperscriptChildAttrs} */
12 | let attrs = {};
13 |
14 | if (children[0]?.constructor?.name === 'Object') {
15 | attrs = children.shift();
16 | }
17 |
18 | children = children.flat(Infinity);
19 |
20 | const el = document.createElement(tagName);
21 |
22 | if (Object.keys(attrs).length > 0) {
23 | bind(el, attrs);
24 | }
25 |
26 | /**
27 | *
28 | * @param {string | number | Element | Node | import('../../types/modules/signal').Signal} child
29 | * @returns {Array}
30 | */
31 | const normalizeChild = (child) => {
32 | /** @type {Array} */
33 | const result = [];
34 |
35 | if (child instanceof Element || child instanceof Node) {
36 | result.push(child);
37 | } else if (child instanceof Signal) {
38 | result.push(...normalizeChild(child()));
39 | child.watch(({ newValue }) => {
40 | const newNormalizedChildren = normalizeChild(newValue);
41 | for (const newNormalizedChild of newNormalizedChildren) {
42 | const oldNormalizedChild = result.shift();
43 | if (oldNormalizedChild != null) {
44 | if (oldNormalizedChild !== newNormalizedChild) {
45 | el.replaceChild(newNormalizedChild, oldNormalizedChild);
46 | }
47 | } else {
48 | el.appendChild(newNormalizedChild);
49 | }
50 | }
51 | for (const oldNormalizedChild of result) {
52 | el.removeChild(oldNormalizedChild);
53 | }
54 | result.push(...newNormalizedChildren);
55 | });
56 | } else if (child instanceof Array) {
57 | for (const childItem of child) {
58 | result.push(...normalizeChild(childItem));
59 | }
60 | } else {
61 | result.push(document.createTextNode(String(child)));
62 | }
63 |
64 | return result;
65 | };
66 |
67 | const fragment = document.createDocumentFragment();
68 |
69 | for (const child of children) {
70 | fragment.append(...normalizeChild(child));
71 | }
72 |
73 | el.appendChild(fragment);
74 |
75 | /** @type {T} */
76 | return el;
77 | };
78 |
79 | return { h };
80 | };
81 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/intersection-observer.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default ({ params }) => ({
3 | /** @type {import('../../types/modules/intersection-observer').observeIntersection} */
4 | observeIntersection: (element, callback, options) => {
5 | const observer = new IntersectionObserver(callback, {
6 | root: element.closest(`[${params.attributePrefix}intersection-observer-root]`),
7 | rootMargin: '0% 0%',
8 | threshold: [0.0, 0.1],
9 | ...options ?? {}
10 | });
11 |
12 | observer.observe(element);
13 | return observer;
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/logger.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ resolve}, options) => {
3 |
4 | const { ajax, dispatch } = await resolve('ajax', 'event');
5 |
6 | /** @type {import('../../types/modules/logger').Levels[]} */
7 | const enabledLevels = options?.levels ?? ['error'];
8 | let handlerProcessingRequest = false;
9 | /**
10 | * @param {import('../../types/modules/logger').Log} log
11 | * @returns {Promise}
12 | */
13 | const handler = async (log) => {
14 | if (handlerProcessingRequest) {
15 | return;
16 | }
17 |
18 | handlerProcessingRequest = true;
19 | log.url = window.location.href;
20 |
21 | const body = { log };
22 | const logStopped = !dispatch(`logger:${log.type}`, body);
23 |
24 | if (!logStopped) {
25 | await ajax(options.url, { body });
26 | }
27 |
28 | handlerProcessingRequest = false;
29 | };
30 |
31 | for (const level of enabledLevels) {
32 | const originalCall = console[level];
33 | /**
34 | * @param {...any} args
35 | * @returns {void}
36 | */
37 | console[level] = (...args) => {
38 | void handler({ type: level, message: args.join(',') });
39 | originalCall(...args);
40 | };
41 | }
42 |
43 | if (enabledLevels.includes('error')) {
44 | /**
45 | * @param {Event | string} message - The error message or event object.
46 | * @param {string} [file] - The file associated with the error (optional).
47 | * @param {number} [lineNumber] - The line number associated with the error (optional).
48 | * @param {number} [columnNumber] - The column number associated with the error (optional).
49 | * @param {Error} [error] - The Error object representing the error (optional).
50 | * @returns {void}
51 | */
52 | window.onerror = (message, file, lineNumber, columnNumber, error) => {
53 | if (message === 'Script error.') {
54 | // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
55 | return;
56 | }
57 |
58 | void handler({
59 | type: 'error',
60 | message: message instanceof Event ? message.type : message,
61 | file: file ?? null,
62 | lineNumber: lineNumber ?? 0,
63 | columnNumber: columnNumber ?? 0,
64 | stack: error?.stack === undefined ? null : error.stack
65 | });
66 | };
67 |
68 | /**
69 | * Event listener for handling unhandled promise rejections.
70 | *
71 | * @function
72 | * @param {PromiseRejectionEvent} event - The event object representing the unhandled promise rejection.
73 | * @returns {void}
74 | */
75 | window.addEventListener('unhandledrejection', (event) => {
76 | void handler({ type: 'error', message: event.reason });
77 | });
78 | }
79 | };
80 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/mutation-observer.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default ($) => {
3 | /** @type {MutationObserver} */
4 | let rootObserver;
5 | const rootObserverListeners = new Set();
6 |
7 | /**
8 | * @param {MutationRecord[]} mutationRecords
9 | * @param {number[]} allowedNodeTypes
10 | * @returns {import('../../types/modules/mutation-observer').MutationNodes}
11 | */
12 | const processMutationObserverRecords = (mutationRecords, allowedNodeTypes = []) => {
13 | /** @type {Node[]} */
14 | const addedNodes = [];
15 | /** @type {Node[]} */
16 | const removedNodes = [];
17 | /** @type {Node[]} */
18 | const movedNodes = [];
19 |
20 | /**
21 | * @param {number} nodeType
22 | * @returns {boolean}
23 | */
24 | const isAllowedNode = (nodeType) => allowedNodeTypes.includes(nodeType);
25 |
26 | for (const mutation of mutationRecords) {
27 | for (const removedNode of removedNodes) {
28 | if (isAllowedNode(removedNode.nodeType)) {
29 | removedNodes.push(removedNode);
30 | }
31 | }
32 |
33 | for (const addedNode of mutation.addedNodes) {
34 | if (!isAllowedNode(addedNode.nodeType)) {
35 | continue;
36 | }
37 |
38 | if (removedNodes.includes(addedNode)) {
39 | movedNodes.push(addedNode);
40 | } else {
41 | addedNodes.push(addedNode);
42 | }
43 | }
44 | }
45 |
46 | return {
47 | addedNodes,
48 | removedNodes,
49 | movedNodes
50 | };
51 | };
52 |
53 | /** @type {import('../../types/modules/mutation-observer').createMutationObserver} */
54 | const createMutationObserver = (root, listener, options) => {
55 | const observer = new MutationObserver((mutationRecords) => {
56 | const nodes = processMutationObserverRecords(mutationRecords);
57 | listener(nodes);
58 | });
59 |
60 | observer.observe(root, {
61 | childList: true,
62 | subtree: true,
63 | ...options ?? {}
64 | });
65 |
66 | return observer;
67 | };
68 |
69 | /** @type {import('../../types/modules/mutation-observer').observeMutations} */
70 | const observeMutations = (listener) => {
71 | if (rootObserver === undefined) {
72 | rootObserver = new MutationObserver((mutationRecords) => {
73 | const nodes = processMutationObserverRecords(mutationRecords, [1]);
74 |
75 | for (const listener of rootObserverListeners) {
76 | listener(nodes);
77 | }
78 | });
79 |
80 | rootObserver.observe($.root, {
81 | childList: true,
82 | subtree: true,
83 | });
84 | }
85 |
86 | if (!rootObserverListeners.has(listener)) {
87 | rootObserverListeners.add(listener);
88 | }
89 |
90 | return () => rootObserverListeners.delete(listener);
91 | };
92 |
93 | return {
94 | observeMutations,
95 | createMutationObserver
96 | };
97 | };
98 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/offset.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default () => {
3 | return {
4 | /** @type {import('../../types/modules/offset').offset} */
5 | offset: (element) => {
6 | const rect = element.getBoundingClientRect();
7 | const defaultView = element.ownerDocument.defaultView;
8 |
9 | return {
10 | top: rect.top + (defaultView !== null ? defaultView.scrollY : 0),
11 | bottom: rect.bottom + (defaultView !== null ? defaultView.scrollY : 0),
12 | left: rect.left + (defaultView !== null ? defaultView.scrollX : 0),
13 | right: rect.right + (defaultView !== null ? defaultView.scrollX : 0)
14 | };
15 | }
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/scope.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ($) => {
3 | const { observeMutations } = await $.resolve('mutation-observer');
4 | const scopeKey = '__signalizeScope';
5 |
6 | /** @type {import('../../types/modules/scope').Scope} */
7 | class Scope {
8 | /**
9 | * @readonly
10 | * @type {Set}
11 | */
12 | #cleanups = new Set();
13 |
14 | /**
15 | * @type {Record}
16 | */
17 | #data = {};
18 |
19 | /**
20 | * @readonly
21 | * @type {import('../../types/Signalize').Signalize}
22 | */
23 | $ = $;
24 |
25 | /**
26 | * @readonly
27 | * @type {import('../../types/modules/scope').SignalizeNode}
28 | */
29 | $el;
30 |
31 | /**
32 | * @type {Scope|undefined}
33 | */
34 | $parentScope
35 |
36 | /**
37 | * @constructor
38 | * @param {Object} options - The options object for initializing the instance.
39 | * @param {import('../../types/modules/scope').SignalizeNode} options.node - The Node associated with the instance.
40 | */
41 | constructor ({ node }) {
42 | this.$el = node;
43 | node[scopeKey] = this;
44 | }
45 |
46 | /** @type {import('../../types/modules/scope').$data} */
47 | get $data() {
48 | return new Proxy(this.#data, {
49 | /**
50 | * @param {Record} target
51 | * @param {string} key
52 | */
53 | get: (target, key) => {
54 | return target[key] ?? this.$parentScope?.$data[key];
55 | },
56 | /**
57 | * @param {Record} target
58 | * @param {string} key
59 | * @param {any} val
60 | */
61 | set: (target, key, val) => {
62 | target[key] = val;
63 | this[key] = val;
64 | return true;
65 | },
66 | /**
67 | * @param {Record} target
68 | * @param {string} key
69 | */
70 | deleteProperty: (target, key) => {
71 | delete target[key];
72 | delete this[key];
73 | return true;
74 | }
75 | });
76 | }
77 |
78 | /** @type {import('../../types/modules/scope').$data} */
79 | set $data (data) {
80 | for (const key in this.$data) {
81 | if (key in data) {
82 | continue;
83 | }
84 |
85 | delete this.$data[key];
86 | }
87 |
88 | for (const key in data) {
89 | this.$data[key] = data[key];
90 | }
91 | }
92 |
93 | /** @type {import('../../types/modules/scope').$cleanup} */
94 | $cleanup = (callback) => {
95 | if (callback !== undefined) {
96 | this.#cleanups.add(callback);
97 | return;
98 | }
99 |
100 | /**
101 | * @param {import('../../types/modules/scope').SignalizeNode} element
102 | * @returns {void}
103 | */
104 | const cleanChildren = (element) => {
105 | for (const child of [...element.childNodes]) {
106 | scope(child)?.$cleanup();
107 |
108 | if (child instanceof Element && child.childNodes.length > 0) {
109 | cleanChildren(child);
110 | }
111 | }
112 | };
113 |
114 | for (const cleanup of this.#cleanups) {
115 | cleanup();
116 | }
117 |
118 | this.#cleanups.clear();
119 |
120 | cleanChildren(this.$el);
121 | };
122 |
123 | }
124 |
125 | /** @type {import('../../types/modules/scope').scope} */
126 | const scope = (node, init) => {
127 | if (typeof init === 'function') {
128 | init(node[scopeKey] ?? new Scope({ node }));
129 | }
130 |
131 | return node[scopeKey] ?? undefined;
132 | };
133 |
134 | observeMutations(({ removedNodes }) => {
135 | for (const removedNode of removedNodes) {
136 | scope(removedNode)?.$cleanup();
137 | }
138 | });
139 |
140 | return { scope };
141 | };
142 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/signal.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default () => {
3 | /**
4 | * @template T
5 | * @type {import('../../types/modules/signal').Signal}
6 | */
7 | class Signal {
8 | /** @type {T} */
9 | value;
10 |
11 | /** @type {import('../../types/modules/signal').SignalWatchers} */
12 | watchers = {
13 | beforeSet: new Set(),
14 | afterSet: new Set(),
15 | onGet: new Set()
16 | };
17 |
18 | /** @param {T} defaultValue */
19 | constructor(defaultValue) {
20 | this.value = defaultValue;
21 |
22 | // In order to make signal callable and to be able to check if created signal is instanceof Signal
23 | // We have to create this function and bind a protype of the Signal to it
24 | const callable = (...args) => {
25 | if (args.length === 1) {
26 | this.#set(args[0]);
27 | return this.value;
28 | }
29 | return this.#get();
30 | };
31 |
32 | Object.setPrototypeOf(callable, this);
33 |
34 | return callable;
35 | }
36 |
37 | /**
38 | * @returns {T}
39 | */
40 | #get = () => {
41 | for (const watcher of this.watchers.onGet) {
42 | watcher({ newValue: this.value, oldValue: this.value });
43 | }
44 |
45 | return this.value;
46 | };
47 |
48 | /**
49 | * @param {T} newValue
50 | * @returns {void}
51 | */
52 | #set = (newValue) => {
53 | const oldValue = this.value;
54 |
55 | let settable = true;
56 |
57 | for (const watcher of this.watchers.beforeSet) {
58 | const watcherData = watcher({ newValue, oldValue });
59 | if (typeof watcherData !== 'undefined') {
60 | settable = watcherData.settable ?? settable;
61 | newValue = watcherData.value ?? newValue;
62 | }
63 |
64 | if (!settable) {
65 | break;
66 | }
67 | }
68 |
69 | if (!settable) {
70 | return;
71 | }
72 |
73 | this.value = newValue;
74 |
75 | for (const watcher of this.watchers.afterSet) {
76 | watcher({ newValue, oldValue });
77 | }
78 | };
79 |
80 | /** @type {import('../../types/modules/signal').SetSignalWatcher} */
81 | watch = (listener, options = {}) => {
82 | const execution = options.execution ?? 'afterSet';
83 |
84 | if (options.immediate ?? false) {
85 | const watcherData = listener({ newValue: this.value });
86 | if (typeof watcherData !== 'undefined' && execution === 'beforeSet' && (watcherData.settable ?? true)) {
87 | this.value = watcherData.value;
88 | }
89 | }
90 |
91 | this.watchers[execution].add(listener);
92 |
93 | return () => {
94 | this.watchers[execution].delete(listener);
95 | };
96 | };
97 |
98 | /**
99 | * @returns {string}
100 | */
101 | toString = () => String(this.#get());
102 |
103 | /**
104 | * @returns {T}
105 | */
106 | toJSON = () => this.#get();
107 |
108 | /**
109 | * @returns { T}
110 | */
111 | valueOf = () => this.#get();
112 | }
113 |
114 | /** @type {import('../../types/modules/signal').signal} */
115 | const signal = (defaultValue) => new Signal(defaultValue);
116 |
117 | return { signal, Signal };
118 | };
119 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/sizes.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default () => ({
3 | height: (element) => {
4 | if (element === document) {
5 | return window.innerHeight;
6 | }
7 |
8 | return element instanceof Element ? element.getBoundingClientRect().height : 0;
9 | },
10 | width: (element) => {
11 | if (element === document) {
12 | return window.innerWidth;
13 | }
14 |
15 | return element instanceof Element ? element.getBoundingClientRect().width : 0;
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/snippets.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ params, resolve, root }) => {
3 |
4 | const { dispatch } = await resolve('event');
5 | const snippetAttribute = `${params.attributePrefix}snippet`;
6 | const snippetAttributeDirective = `${snippetAttribute}${params.attributeSeparator}`;
7 | const snippetStateAttribute = `${snippetAttributeDirective}state`;
8 | const snippetActionAttribute = `${snippetAttributeDirective}action`;
9 |
10 | /**
11 | * @param {string} html
12 | * @param {DOMParserSupportedType} type
13 | * @returns {Document}
14 | */
15 | const parseHtml = (html, type = 'text/html') => (new DOMParser()).parseFromString(html, type);
16 |
17 | /** @type {import('../../types/modules/snippets').redrawSnippet} */
18 | const redrawSnippet = async (content, options = {}) => {
19 | const fragment = parseHtml(content);
20 | let snippets = [...fragment.querySelectorAll(`[${snippetAttribute}]`)];
21 | /** @type {Promise[]} */
22 | const redrawPromisses = [];
23 | /** @type {Promise[]} */
24 | const stylePromises = [];
25 |
26 | const redrawSnippet = async (newSnippet) => {
27 | const snippetId = newSnippet.getAttribute(snippetAttribute);
28 | let existingSnippet = root.querySelector(`[${snippetAttribute}="${snippetId}"]`);
29 |
30 | const snippetConfig = {
31 | snippetId,
32 | snippetActions: newSnippet.getAttribute(`${snippetActionAttribute}`)?.split(' ') ?? ['replace'],
33 | newSnippet,
34 | existingSnippet,
35 | ...snippetId ? (options?.snippets?.[snippetId] ?? {}) : {}
36 | };
37 |
38 | const { snippetActions } = snippetConfig;
39 |
40 | existingSnippet.setAttribute(snippetStateAttribute, 'redrawing');
41 | dispatch('snippets:redraw:start', snippetConfig);
42 |
43 | while(snippetActions.length) {
44 | const snippetAction = snippetActions.shift();
45 | if (snippetAction === 'replace') {
46 | newSnippet.setAttribute(snippetStateAttribute, 'redrawing');
47 | existingSnippet.replaceWith(newSnippet);
48 | existingSnippet = newSnippet;
49 | } else if (snippetAction === 'replace-children') {
50 | existingSnippet.setAttribute(snippetStateAttribute, 'redrawing');
51 | existingSnippet.innerHTML = newSnippet.innerHTML;
52 |
53 | } else if (snippetAction === 'append-children') {
54 | const childrenFragment = new DocumentFragment();
55 | while (newSnippet.firstChild != null) {
56 | childrenFragment.append(newSnippet.firstChild);
57 | }
58 |
59 | existingSnippet.append(childrenFragment);
60 | } else if (snippetAction === 'prepend-children') {
61 | const childrenFragment = new DocumentFragment();
62 | while (newSnippet.lastChild != null) {
63 | childrenFragment.append(newSnippet.lastChild);
64 | }
65 |
66 | existingSnippet.prepend(childrenFragment);
67 | } else if (snippetAction === 'sync-attributes') {
68 | for (const attribute of newSnippet.attributes) {
69 | existingSnippet.setAttribute(attribute.name, attribute.value);
70 | }
71 | }
72 | }
73 |
74 | for (const script of existingSnippet.querySelectorAll('script')) {
75 | const scriptTypeAttribute = script.getAttribute('type');
76 |
77 | if (scriptTypeAttribute && !['module', 'text/javascript'].includes(scriptTypeAttribute)) {
78 | continue;
79 | }
80 |
81 | const scriptElement = document.createElement('script');
82 | scriptElement.innerHTML = script.innerHTML;
83 | scriptElement.async = false;
84 |
85 | for (const { name, value } of [...script.attributes]) {
86 | scriptElement.setAttribute(name, value);
87 | }
88 |
89 | script.replaceWith(scriptElement);
90 | }
91 |
92 | await Promise.all(redrawPromisses);
93 |
94 | dispatch('snippets:redraw:end', snippetConfig);
95 |
96 | existingSnippet.setAttribute(snippetStateAttribute, 'redrawed');
97 | }
98 |
99 | const redraw = async () => {
100 | snippets = snippets.filter((newSnippet) => {
101 | const snippetId = newSnippet.getAttribute(snippetAttribute);
102 | const existingSnippet = root.querySelector(`[${snippetAttribute}="${snippetId}"]`);
103 |
104 | if (existingSnippet === null) {
105 | return false;
106 | }
107 |
108 | for (const styleLink of newSnippet.querySelectorAll('link[rel="stylesheet"]')) {
109 | stylePromises.push(new Promise((resolve, reject) => {
110 | /** @type {HTMLLinkElement} */
111 | const stylePreload = styleLink.cloneNode();
112 | stylePreload.setAttribute('rel', 'preload');
113 | stylePreload.setAttribute('as', 'style');
114 | stylePreload.onload = () => {
115 | stylePreload.remove();
116 | resolve(true);
117 | }
118 | stylePreload.onerror = () => {
119 | stylePreload.remove();
120 | reject(false);
121 | }
122 | document.body.appendChild(stylePreload);
123 | }));
124 | }
125 |
126 | return true;
127 | })
128 |
129 | await Promise.all(stylePromises);
130 |
131 | while (snippets.length > 0) {
132 | const newSnippet = snippets.shift();
133 | const parentSnippet = newSnippet?.parentNode?.closest(`[${snippetAttribute}]`);
134 | if (parentSnippet) {
135 | const parentSnippetActions = parentSnippet.getAttribute(snippetActionAttribute) ?? null;
136 | let shouldSkipSnippet = parentSnippetActions === null;
137 |
138 | if (!shouldSkipSnippet) {
139 | for(const action of ['replace', 'replace-children', 'append-children', 'prepend-children']) {
140 | shouldSkipSnippet = parentSnippetActions.includes(action);
141 |
142 | if (shouldSkipSnippet) {
143 | break;
144 | }
145 | }
146 | }
147 |
148 | if (shouldSkipSnippet) {
149 | continue;
150 | }
151 | }
152 |
153 | redrawPromisses.push(redrawSnippet(newSnippet));
154 | }
155 | };
156 |
157 | if (document?.startViewTransition === undefined || options?.transitionsEnabled === false) {
158 | await redraw();
159 | } else {
160 | const transition = document.startViewTransition(redraw);
161 | dispatch('snippets:redraw:transition:start', transition);
162 | await transition.finished;
163 | dispatch('snippets:redraw:transition:end');
164 | }
165 |
166 | };
167 |
168 | return { redrawSnippet };
169 | };
170 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/spa.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ params, resolve, root }, config) => {
3 |
4 | const { dispatch, ajax, redrawSnippet, on, customEventListener, customEvent } = await resolve(
5 | 'dom/ready',
6 | 'event',
7 | 'ajax',
8 | 'snippets'
9 | );
10 |
11 | const spaAttribute = `${params.attributePrefix}spa`;
12 | const spaUrlAttribute = `${spaAttribute}${params.attributeSeparator}url`;
13 | const spaIgnoreAttribute = `${spaAttribute}${params.attributeSeparator}ignore`;
14 | const spaStateActionAttribute = `${spaAttribute}${params.attributeSeparator}state-action`;
15 | const spaProcessingLabelAttribute = `${spaAttribute}${params.attributeSeparator}processing-label`;
16 | const spaConfirmAttribute = `${spaAttribute}${params.attributeSeparator}confirm-message`;
17 | const spaHttpMethodAttribute = `${spaAttribute}${params.attributeSeparator}http-method`;
18 | const spaMetaCacheNameAttribute = `${spaAttribute}${params.attributeSeparator}cache-control`;
19 | const spaHeaderPrefix = 'X-Spa-';
20 | const spaCacheHeader = config?.cacheHeader ?? `${spaHeaderPrefix}Cache-Control`;
21 | const spaAppVersionHeader = config?.appVersionHeader ?? `${spaHeaderPrefix}App-Version`;
22 | const spaTransitionsHeader = config?.appVersionHeader ?? `${spaHeaderPrefix}Transitions`;
23 | const defaultStateAction = 'push';
24 |
25 | /** @type {import('../../types/modules/spa').HistoryState|undefined} */
26 | let currentState;
27 | /** @type {AbortController|undefined} */
28 | let abortNavigationRequestController;
29 | const spaVersion = null;
30 | const host = window.location.host;
31 | /** @type {import('../../types/modules/spa').ResponseCache} */
32 | const responseCache = {};
33 |
34 | /**
35 | *
36 | * @param {string} urlString
37 | * @returns {URL|null}
38 | */
39 | const createUrl = (urlString) => {
40 | try {
41 | const url = new URL(urlString, getCurrentLocation());
42 | return url;
43 | } catch (error) {
44 | console.error(error);
45 | }
46 |
47 | return null;
48 | };
49 |
50 | /**
51 | *
52 | * @param {any} content
53 | * @returns {boolean}
54 | */
55 | const isJson = (content) => {
56 | try {
57 | JSON.parse(content);
58 | } catch (e) {
59 | return false;
60 | }
61 | return true;
62 | };
63 |
64 | let firstNavigationTriggered = false;
65 |
66 | customEventListener('spa:page:ready', ({
67 | on: ({ listener }) => {
68 | root.addEventListener('spa:page:ready', listener, { passive: true });
69 |
70 | if (!firstNavigationTriggered) {
71 | listener(customEvent('spa:page:ready', currentState));
72 | }
73 | },
74 | off: ({ listener }) => {
75 | root.removeEventListener('spa:page:ready', listener);
76 | }
77 | }));
78 |
79 | /** @type {import('../../types/modules/spa').navigate} */
80 | const navigate = async (data) => {
81 | updateCurrentState();
82 |
83 | if (typeof data === 'string') {
84 | data = { url: data };
85 | }
86 |
87 | firstNavigationTriggered = true;
88 | /** @type {import('../../types/modules/spa.d.ts').NavigationEventData} */
89 | const dispatchEventData = {
90 | ...data,
91 | scrollX: data.scrollX ?? window.scrollX,
92 | scrollY: data.scrollY ?? window.scrollY,
93 | stateAction: data.stateAction ?? defaultStateAction,
94 | error: null
95 | };
96 |
97 | let navigationRequestIsRunning = false;
98 |
99 | if (abortNavigationRequestController !== undefined) {
100 | abortNavigationRequestController.abort();
101 |
102 | if (navigationRequestIsRunning) {
103 | navigationRequestIsRunning = false;
104 | dispatch('spa:request:end', { ...dispatchEventData, navigationResponse: null });
105 | }
106 |
107 | dispatch('spa:navigation:end', dispatchEventData);
108 | }
109 |
110 | abortNavigationRequestController = new AbortController();
111 | const { stateAction = defaultStateAction, httpMethod } = data;
112 | const url = data.url instanceof URL ? data.url : createUrl(data.url);
113 |
114 | if (url === null) {
115 | throw new Error('Error during navigation.');
116 | }
117 |
118 | const currentLocation = getCurrentLocation();
119 | const onlyHashChanged = url.pathname === currentLocation.pathname && url.hash !== currentLocation.hash;
120 | const shouldTriggerNavigation = !onlyHashChanged;
121 | let urlString = url.toString();
122 |
123 | /** @type {import('../../types/modules/ajax.d.ts').AjaxReturn} */
124 | let navigationResponse;
125 | /** @type {string|null} */
126 | let responseData = null;
127 |
128 | const urlIsCached = urlString in responseCache;
129 |
130 | if (shouldTriggerNavigation) {
131 | dispatch('spa:navigation:start', { ...dispatchEventData });
132 |
133 | if (urlIsCached) {
134 | responseData = responseCache[urlString];
135 | } else {
136 | dispatch('spa:request:start', { ...dispatchEventData });
137 |
138 | navigationRequestIsRunning = true;
139 | navigationResponse = await ajax(urlString, {
140 | method: httpMethod ?? 'GET',
141 | signal: abortNavigationRequestController.signal,
142 | headers: {
143 | Accept: 'text/html, application/xhtml+xml'
144 | }
145 | });
146 |
147 | navigationRequestIsRunning = false;
148 | abortNavigationRequestController = undefined;
149 | const requestIsWithoutErroor = navigationResponse.error === null;
150 |
151 | if (requestIsWithoutErroor) {
152 | if (navigationResponse.response.redirected) {
153 | urlString = navigationResponse.response.url;
154 | }
155 |
156 | try {
157 | responseData = navigationResponse.response === null ? '' : await navigationResponse.response.text();
158 | } catch (error) {
159 | dispatchEventData.error = error;
160 | console.error(error);
161 | }
162 | } else {
163 | dispatchEventData.error = navigationResponse.error;
164 | dispatch('spa:request:error', { ...navigationResponse, ...dispatchEventData });
165 | }
166 |
167 | dispatch('spa:request:end', { ...navigationResponse, ...dispatchEventData });
168 | }
169 | }
170 |
171 | /** @param {string} urlHash */
172 | const scrollElementIntoView = (urlHash) => {
173 | urlHash = urlHash.slice(1);
174 | const element = root.querySelector(`[id="${urlHash}"]`);
175 | if (element !== null) {
176 | element.scrollIntoView({
177 | block: 'start',
178 | inline: 'nearest'
179 | });
180 | }
181 | }
182 |
183 | const updateDom = async () => {
184 | /** @type {boolean|null} */
185 | let shouldCacheResponse = null;
186 |
187 | /** @type {Headers|undefined} */
188 | const headers = navigationResponse?.response?.headers;
189 |
190 | if (headers !== undefined) {
191 | const cacheHeader = headers.get(spaCacheHeader) ?? null;
192 | if (cacheHeader !== null) {
193 | shouldCacheResponse = cacheHeader !== 'no-cache';
194 | }
195 |
196 | const spaVersionFromHeader = headers.get(spaAppVersionHeader) ?? null;
197 |
198 | if (spaVersionFromHeader !== null && spaVersion !== null && spaVersion !== spaVersionFromHeader) {
199 | dispatch('spa:app-version:changed');
200 | }
201 | }
202 |
203 | if (!isJson(responseData)) {
204 | await redrawSnippet(responseData, {
205 | transitions: headers?.get(spaTransitionsHeader) ?? 'enabled'
206 | });
207 | }
208 |
209 | if (stateAction === 'replace') {
210 | window.history.replaceState(window.history.state, '', urlString);
211 | } else if (stateAction === 'push') {
212 | currentState = {
213 | url: urlString,
214 | spa: true,
215 | scrollX: data.scrollX ?? window.scrollX,
216 | scrollY: data.scrollY ?? window.scrollY
217 | };
218 | window.history.pushState(currentState, '', urlString);
219 | }
220 |
221 | if (shouldCacheResponse === null) {
222 | const metaCacheControlElement = root.querySelector(`meta[name="${spaMetaCacheNameAttribute}"]`);
223 | shouldCacheResponse = !urlIsCached && (
224 | metaCacheControlElement === null || metaCacheControlElement.getAttribute('content') !== 'no-cache'
225 | );
226 | }
227 |
228 | if (shouldCacheResponse && responseData) {
229 | responseCache[urlString] = responseData;
230 | }
231 | };
232 |
233 | if (responseData !== null) {
234 | dispatch('spa:redraw:start', dispatchEventData);
235 | try {
236 | await updateDom();
237 | } catch (e) {
238 | dispatchEventData.error = e;
239 | console.log(e);
240 | }
241 |
242 | dispatch('spa:redraw:end', dispatchEventData);
243 |
244 | let urlHash = window.location.hash ?? null;
245 |
246 | const navigationScrollStopped = dispatch(
247 | 'spa:navigation:beforeScroll',
248 | undefined,
249 | { cancelable: true }
250 | ) === false;
251 |
252 | if (!navigationScrollStopped) {
253 | if (urlHash !== null && urlHash.trim().length > 2) {
254 | console.log()
255 | scrollElementIntoView(url.hash);
256 | } else {
257 | queueMicrotask(() => {
258 | window.scrollTo(data.scrollX ?? 0, data.scrollY ?? 0);
259 | });
260 | }
261 | }
262 | } else if (onlyHashChanged) {
263 | scrollElementIntoView(url.hash);
264 | }
265 |
266 | const error = responseData === null;
267 | const navigationEndData = { ...dispatchEventData, error };
268 |
269 | if (shouldTriggerNavigation) {
270 | dispatch('spa:navigation:end', navigationEndData);
271 |
272 | if (error === false) {
273 | dispatch('spa:page:ready', navigationEndData);
274 | }
275 | }
276 |
277 | return navigationEndData;
278 | };
279 |
280 | /**
281 | * @returns {void}
282 | */
283 | const onPopState = () => {
284 | /** @type {import('../../types/modules/spa.d.ts').HistoryState} */
285 | const state = window.history.state;
286 |
287 | if (!(state?.spa ?? false)) {
288 | return;
289 | }
290 |
291 | if (state.url === undefined || state.url === currentState?.url) {
292 | return;
293 | }
294 |
295 | /** @type {import('../../types/modules/spa.d.ts').NavigationData} */
296 | const navigationConfig = {
297 | url: state.url,
298 | scrollX: state.scrollX,
299 | scrollY: state.scrollY,
300 | stateAction: 'replace'
301 | };
302 |
303 | dispatch('spa:popstate', navigationConfig);
304 |
305 | void navigate(navigationConfig);
306 | };
307 |
308 | const getCurrentLocation = () => {
309 | return new URL(window.location.href);
310 | };
311 |
312 | /**
313 | * @param {MouseEvent} event
314 | * @returns {Promise}
315 | */
316 | const onClick = async (event) => {
317 | if (event.ctrlKey === true || event.metaKey === true) {
318 | return;
319 | }
320 |
321 | /** @type {HTMLAnchorElement} */
322 | const element = event.target.closest('a');
323 | const targetAttribute = element.getAttribute('target');
324 |
325 | if (element.hasAttribute(spaIgnoreAttribute) || element.hasAttribute('download') || ![null, '_self'].includes(targetAttribute)) {
326 | return;
327 | }
328 |
329 | const url = element.getAttribute('href') ??
330 | element.getAttribute(spaUrlAttribute) ??
331 | element.closest('[href]')?.getAttribute('href') ??
332 | element.closest(`[${spaUrlAttribute}]`)?.getAttribute(spaUrlAttribute);
333 |
334 | if (url === null || url === undefined || url.startsWith('#')) {
335 | return;
336 | }
337 |
338 | const parsedOriginalUrl = createUrl(url);
339 |
340 | if (parsedOriginalUrl !== null && parsedOriginalUrl.host !== host) {
341 | return;
342 | }
343 |
344 | const hrefUrl = createUrl(`${window.location.origin}${url}`);
345 | const currentLocation = getCurrentLocation();
346 |
347 | if (hrefUrl === null || hrefUrl.toString() === currentLocation.toString()) {
348 | event.preventDefault();
349 | return;
350 | }
351 |
352 | const clickCanceled = dispatch('spa:click', { element }, { cancelable: true }) === false;
353 |
354 | if (clickCanceled) {
355 | event.preventDefault();
356 | return;
357 | }
358 |
359 | event.preventDefault();
360 |
361 | /** @type {import('../../types/modules/spa.d.ts').StateAction} */
362 | let stateAction = defaultStateAction;
363 | const stateActionAttribute = element.getAttribute(spaStateActionAttribute);
364 |
365 | if (stateActionAttribute) {
366 | if (!['push', 'replace'].includes(stateActionAttribute)) {
367 | throw new Error(`Unknown operation on spa action attribute "${stateAction}".`);
368 | }
369 |
370 | stateAction = stateActionAttribute;
371 | }
372 |
373 | const confirmMessage = element.getAttribute(spaConfirmAttribute);
374 | if (confirmMessage && !confirm(confirmMessage)) {
375 | return;
376 | }
377 |
378 | const processingLabel = element.getAttribute(spaProcessingLabelAttribute);
379 | let previousLabel = null;
380 |
381 | if (processingLabel) {
382 | previousLabel = element.innerHTML;
383 | element.innerHTML = processingLabel;
384 | }
385 |
386 | await navigate({
387 | httpMethod: element.getAttribute(spaHttpMethodAttribute),
388 | url,
389 | stateAction
390 | });
391 |
392 | if (previousLabel !== null) {
393 | element.innerHTML = previousLabel;
394 | }
395 | };
396 |
397 | const updateCurrentState = () => {
398 | currentState = {
399 | spa: true,
400 | url: window.location.pathname,
401 | scrollX: window.scrollX,
402 | scrollY: window.scrollY
403 | };
404 |
405 | window.history.replaceState(currentState, '', window.location.href);
406 | };
407 |
408 | on('dom:ready', () => {
409 | updateCurrentState();
410 |
411 | dispatch('spa:page:ready', currentState);
412 |
413 | on('click', `a[href], [${spaUrlAttribute}]`, onClick);
414 |
415 | window.addEventListener('popstate', onPopState);
416 | });
417 |
418 | return { navigate };
419 | };
420 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/strings/cases.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../../types/Signalize').Module} */
2 | export default () => ({
3 | /** @type {import('../../../types/modules/strings/cases').dashCase} */
4 | dashCase: (str) => str.replace(/[A-Z]/g, (token) => '-' + token.toLowerCase())
5 | });
6 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/task.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default () => {
3 | const deadlineInterval = 50;
4 |
5 | /** @type {CallableFunction[]} */
6 | const tasks = [];
7 |
8 | /** @returns {Promise} */
9 | const yieldToMain = async () => {
10 | await new Promise((resolve) => window.setTimeout(resolve, 0));
11 | };
12 |
13 | let processing = false;
14 |
15 | return {
16 | /** @type {import('../../types/modules/task').task} */
17 | task: (callback) => {
18 | tasks.push(callback);
19 |
20 | if (processing) {
21 | return;
22 | }
23 |
24 | processing = true;
25 | void (async () => {
26 | let deadline = window.performance.now() + deadlineInterval;
27 |
28 | while (tasks.length > 0) {
29 | if (window.performance.now() >= deadline || (
30 | typeof window.navigator.scheduling !== 'undefined' && window.navigator.scheduling.isInputPending()
31 | )) {
32 | await yieldToMain();
33 | deadline = window.performance.now() + deadlineInterval;
34 | continue;
35 | }
36 |
37 | const callback = tasks.shift();
38 |
39 | if (typeof callback !== 'function') {
40 | throw new Error('Task must be a callable function.');
41 | }
42 |
43 | callback();
44 | }
45 | processing = false;
46 | })();
47 | }
48 | };
49 | };
50 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/viewport.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default async ({ resolve }) => {
3 |
4 | const { offset, height } = await resolve('offset', 'sizes');
5 |
6 | return {
7 | /** @type {import('../../types/modules/viewport').isInViewport} */
8 | isInViewport: (element) => {
9 | const windowTop = window.scrollY;
10 | const windowBottom = windowTop + window.innerHeight;
11 | const elementTop = offset(element).top;
12 | const elementBottom = elementTop + height(element);
13 |
14 | return {
15 | top: windowTop < elementTop && elementTop < windowBottom,
16 | bottom: windowTop < elementBottom && elementBottom < windowBottom,
17 | whole: windowBottom >= elementBottom && windowTop <= elementTop
18 | };
19 | }
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/packages/signalizejs/src/modules/visibility.js:
--------------------------------------------------------------------------------
1 | /** @type {import('../../types/Signalize').Module} */
2 | export default () => ({
3 | /** @type {import('../../types/modules/visibility').isVisible} */
4 | isVisible: (element) => {
5 | if (element.getClientRects().length !== 0) {
6 | return true;
7 | }
8 |
9 | return element.offsetWidth !== 0 || element.offsetHeight !== 0;
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/packages/signalizejs/tests/height.spec.mjs:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('width', async ({ page }) => {
4 | await page.goto('/packages/signalizejs/tests/pages/height.html');
5 | const result = await (await page.locator('html')).first().getAttribute('result');
6 |
7 | await expect(result).toEqual('200');
8 | });
9 |
--------------------------------------------------------------------------------
/packages/signalizejs/tests/pages/height.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/packages/signalizejs/tests/pages/width.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/packages/signalizejs/tests/width.spec.mjs:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('width', async ({ page }) => {
4 | await page.goto('/packages/signalizejs/tests/pages/width.html');
5 | const result = await (await page.locator('html')).first().getAttribute('result');
6 |
7 | await expect(result).toEqual('200');
8 | });
9 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/Signalize.d.ts:
--------------------------------------------------------------------------------
1 | import { Signalize as SignalizeInstance } from "../src/Signalize";
2 | import { AjaxModule } from "./modules/ajax";
3 | import { BindModule } from "./modules/bind";
4 | import { ComponentModule } from "./modules/component";
5 | import { DialogModule } from "./modules/dialog";
6 | import { DirectivesModule } from "./modules/directives";
7 | import { DomReadyModule } from "./modules/dom/ready";
8 | import { DomTraverserDomModule } from "./modules/dom/traverser";
9 | import { EvaluatorModule } from "./modules/evaluator";
10 | import { EventModule } from "./modules/event";
11 | import { HyperscriptModule } from "./modules/hyperscript";
12 | import { IntersectionObserverModule } from "./modules/intersection-observer";
13 | import { MutationObserverModule } from "./modules/mutation-observer";
14 | import { OffsetModule } from "./modules/offset";
15 | import { ScopeModule } from "./modules/scope";
16 | import { SignalModule } from "./modules/signal";
17 | import { SizesModule } from "./modules/sizes";
18 | import { SnippetsModule } from "./modules/snippets";
19 | import { SpaModule } from "./modules/spa";
20 | import { StringsCasesModule } from "./modules/strings/cases";
21 | import { TaskModule } from "./modules/task";
22 | import { ViewportModule } from "./modules/viewport";
23 | import { VisibilityModule } from "./modules/visibility";
24 |
25 | export type Globals = Record;
26 |
27 | export type Params = Record;
28 |
29 | export type Modules = Array] | [string, Function] | [string, Function, Record]>;
30 |
31 | /** Module resolver definition */
32 | export type ModulesResolver = (moduleName: string) => Promise
33 |
34 | export type Module, C = Record | undefined> = (signalize: Signalize, config: C) => F | Promise;
35 |
36 | export type ModulesToResolve = Array | Function, Record | undefined] | Record>;
37 |
38 | export type InitedCallback = CallableFunction;
39 |
40 | export interface Root extends Node {
41 | __signalize: SignalizeInstance
42 | }
43 |
44 | /**
45 | * Resolve functionality from modules.
46 | */
47 | export type Resolve = (...modules: ModulesToResolve) => Promise;
48 |
49 | export interface ResolvableFunctionality extends
50 | AjaxModule,
51 | BindModule,
52 | ComponentModule,
53 | DialogModule,
54 | DirectivesModule,
55 | DomReadyModule,
56 | DomTraverserDomModule,
57 | EvaluatorModule,
58 | EventModule,
59 | HyperscriptModule,
60 | IntersectionObserverModule,
61 | MutationObserverModule,
62 | OffsetModule,
63 | ScopeModule,
64 | SignalModule,
65 | SizesModule,
66 | SnippetsModule,
67 | SpaModule,
68 | StringsCasesModule,
69 | TaskModule,
70 | ViewportModule,
71 | VisibilityModule { }
72 |
73 | export interface SignalizeConfig {
74 | /** The root element or document where Signalize will be instantiated. */
75 | root?: Root;
76 | /** Parameters that can be accessed by modules within Signalize */
77 | params?: Params
78 | /** Global variables used inside modules functions.
79 | * This object can also be used to prevent polution of the window object.
80 | */
81 | globals?: Globals;
82 | /** Modules that will be inited instantly. */
83 | modules?: Modules;
84 | /** The id of the Signalize instance for imports to prevent collisions. */
85 | instanceId?: string;
86 | /** Asynchronous Modules resolver. */
87 | resolver?: ModulesResolver;
88 | }
89 |
90 | export declare class Signalize {
91 | constructor(options: SignalizeConfig);
92 | root: Root;
93 | globals: Globals;
94 | params: Params;
95 | inited: (callback: InitedCallback) => Promise;
96 | resolve: Resolve;
97 | }
98 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './modules/ajax';
2 | export * from './modules/bind';
3 | export * from './modules/component';
4 | export * from './modules/dialog';
5 | export * from './modules/directives';
6 | export * from './modules/dom/ready';
7 | export * from './modules/dom/traverser';
8 | export * from './modules/evaluator';
9 | export * from './modules/event';
10 | export * from './modules/height';
11 | export * from './modules/hyperscript';
12 | export * from './modules/intersection-observer';
13 | export * from './modules/visibility';
14 | export * from './modules/logger';
15 | export * from './modules/mutation-observer';
16 | export * from './modules/offset';
17 | export * from './modules/scope';
18 | export * from './modules/signal';
19 | export * from './modules/snippets';
20 | export * from './modules/spa';
21 | export * from './modules/strings/cases';
22 | export * from './modules/task';
23 | export * from './modules/viewport';
24 | export * from './modules/sizes';
25 | export * from './Signalize';
26 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/ajax.d.ts:
--------------------------------------------------------------------------------
1 | /** Represents the result of a fetch operation. */
2 | export interface AjaxReturn {
3 | /** The response from the fetch operation (or null if an error occurred). */
4 | response: Response | null;
5 | /** Any error that occurred during the fetch operation. */
6 | error: Error | null;
7 | }
8 |
9 | /** Options for configuring a fetch-related plugin. */
10 | export interface AjaxModuleConfig {
11 | /** The value for the 'Requested-With' header. */
12 | requestedWithHeader?: string;
13 | /** The value for the 'Accept' header. */
14 | acceptHeader?: string;
15 | }
16 |
17 | /**
18 | * Performs a fetch operation with the given resource and options.
19 | * Args are the same line in Native Fetch.
20 | *
21 | * https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
22 | */
23 | export type ajax = (resource: RequestInfo | URL, options?: RequestInit) => Promise;
24 |
25 | export interface AjaxModule {
26 | ajax: ajax
27 | }
28 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/bind.d.ts:
--------------------------------------------------------------------------------
1 | import type { Signal } from '../../types/modules/signal';
2 |
3 | /** Interface defining configuration for an attribute. */
4 | export interface AttributeConfig {
5 | /** Optional setter function for the attribute. */
6 | set?: (value: any) => void;
7 | /** Optional getter function for the attribute. */
8 | get?: () => any;
9 | }
10 |
11 | /** Function type to bind attributes to an element. */
12 | export type bind = (
13 | element: HTMLElement,
14 | attributes: Record>
15 | ) => void;
16 |
17 | export interface BindModule {
18 | bind: bind
19 | }
20 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/component.d.ts:
--------------------------------------------------------------------------------
1 | import { Scope } from "./scope";
2 |
3 | /** Life cycle listener function type. */
4 | export type LifeCycleListener = () => void;
5 |
6 | /* Array of life cycle listeners. */
7 | export type LifeCycleListeners = LifeCycleListener[];
8 |
9 | /** Function type for connecting to life cycle events. */
10 | export type setupCallbackLifeCycleConnector = (listener: LifeCycleListener) => void;
11 |
12 | /** Setup callback parameters interface. */
13 | export interface SetupCallbackParams {
14 | /** Connect to connected life cycle */
15 | $connected: setupCallbackLifeCycleConnector;
16 | /** Connect to disconnected life cycle */
17 | $disconnected: setupCallbackLifeCycleConnector;
18 | /** Connect to adopted life cycle */
19 | $adopted: setupCallbackLifeCycleConnector;
20 | }
21 |
22 | /** Component props function type. */
23 | export type ComponentProps = () => Record;
24 |
25 | /** Component setup function. */
26 | export type setupCallback = (data: SetupCallbackParams) => void | Promise;
27 |
28 | export type $refs = Record;
29 |
30 | /** Component configuration options interface */
31 | export interface ComponentOptions {
32 | /** Component props definition. */
33 | props?: Record | string[] | ComponentProps;
34 | /** Function for defining component logic. */
35 | setup?: setupCallback;
36 | /** List of web components, that must be defined before this component is inited */
37 | components?: string[];
38 | /**
39 | * Use shadow root.
40 | * https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
41 | */
42 | shadow?: ShadowRootInit;
43 | }
44 |
45 | export type $props = Record;
46 |
47 | export type $propsAliases = Record;
48 |
49 | export interface ComponentScope extends Scope {
50 | $refs: $refs,
51 | $props: $props;
52 | $propsAliases: $propsAliases;
53 | _setuped: boolean;
54 | }
55 |
56 | /** Creates a custom Web Component with the specified name and options. */
57 | export type component = (name: string, optionsOrSetup?: ComponentOptions | setupCallback) => typeof HTMLElement;
58 |
59 | export interface ComponentModuleConfig {
60 | componentPrefix?: string
61 | }
62 |
63 | export interface ComponentModule {
64 | component: component
65 | }
66 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/dialog.d.ts:
--------------------------------------------------------------------------------
1 | /** Function type to retrieve a dialog element by ID. */
2 | export type getDialog = (id: string) => HTMLDialogElement | null;
3 |
4 | /**
5 | * Options for opening a dialog.
6 | */
7 | export interface OpenDialogOptions {
8 | /** Whether to open the dialog modelessly (default: false). */
9 | modelessly?: boolean;
10 | /** Whether the dialog is closable (default: true). */
11 | closable?: boolean;
12 | }
13 |
14 | /** Function type to open a dialog. */
15 | export type openDialog = (
16 | dialogOrId: string | HTMLDialogElement,
17 | options?: OpenDialogOptions
18 | ) => HTMLDialogElement | null;
19 |
20 | /** Function type to close a dialog. */
21 | export type closeDialog = (dialogOrId: string | HTMLDialogElement) => HTMLDialogElement | null;
22 |
23 | export interface DialogModule {
24 | openDialog: openDialog;
25 | getDialog: getDialog;
26 | closeDialog: closeDialog;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/directives.d.ts:
--------------------------------------------------------------------------------
1 | import type { Scope } from "./scope";
2 |
3 | /** Callback function type for a directive */
4 | export type DirectiveCallback = (data: DirectiveCallbackData) => Promise | void;
5 |
6 | /** Data passed to a directive callback */
7 | export interface DirectiveCallbackData {
8 | scope: Scope;
9 | /** Result of matching a regular expression */
10 | matches: RegExpMatchArray;
11 | /** Attribute associated with the directive */
12 | attribute: Attr;
13 | }
14 |
15 | /** Parameters for a directive matcher */
16 | export interface DirectiveMatcherParameters {
17 | /** Element associated with the directive */
18 | element: Element;
19 | /** Attribute associated with the directive */
20 | attribute: Attr;
21 | }
22 |
23 | /** Return type of a directive matcher */
24 | export type DirectiveMatcherReturn = RegExp | undefined;
25 |
26 | /** Directive matcher function type */
27 | export type DirectiveMatcher = (params: DirectiveMatcherParameters) => DirectiveMatcherReturn;
28 |
29 | /** Options for processing an HTML element */
30 | export interface ProcessElementOptions {
31 | /** Element to be processed */
32 | element: Element;
33 | }
34 |
35 | /** Directive definition with matcher and callback */
36 | export interface Directive {
37 | /** Optional matcher for the directive */
38 | matcher?: RegExp | DirectiveMatcher;
39 | /** Callback function for the directive */
40 | callback: DirectiveCallback;
41 | }
42 |
43 | /** Registered directive with enforced matcher function type */
44 | export interface RegisteredDirective extends Directive {
45 | /** Matcher function for the directive (enforced) */
46 | matcher?: DirectiveMatcher;
47 | }
48 |
49 | /** Options for processing directives within a DOM tree */
50 | export interface ProcessDirectiveOptions {
51 | /** Root element of the DOM tree to process */
52 | root: Element;
53 | /** Optional array of directive names to process */
54 | directives?: string[];
55 | /** Optional array of directive names to process */
56 | onlyRoot?: boolean;
57 | }
58 |
59 | /** Options for configuring a directive-related plugin */
60 | export interface PluginOptions {
61 | /** Optional start marker for prerendered blocks */
62 | prerenderedBlockStart?: string;
63 | /** Optional end marker for prerendered blocks */
64 | prerenderedBlockEnd?: string;
65 | }
66 |
67 | /** Asynchronously processes directives within a DOM tree based on the specified options. */
68 | export type processDirectives = (options?: ProcessDirectiveOptions) => Promise;
69 |
70 | /** Defines a custom directive with the specified name, matcher, and callback. */
71 | export type directive = (name: string, options: Omit & { matcher?: DirectiveMatcher }) => void;
72 |
73 | /** Retrieves prerendered nodes from the specified HTML element. */
74 | export type getPrerenderedNodes = (element: Element) => Node[];
75 |
76 | export interface DirectivesModule {
77 | getPrerenderedNodes: getPrerenderedNodes;
78 | processDirectives: processDirectives;
79 | directive: directive;
80 | }
81 |
82 | export interface DirectivesModuleConfig {
83 | prerenderedBlockStart?: string;
84 | prerenderedBlockEnd?: string;
85 | }
86 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/dom/ready.d.ts:
--------------------------------------------------------------------------------
1 | export type isDomReady = () => boolean;
2 |
3 | export interface DomReadyModule {
4 | isDomReady: isDomReady
5 | }
6 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/dom/traverser.d.ts:
--------------------------------------------------------------------------------
1 | export type traverseRootCallback = (node: Node) => Promise
2 |
3 | /** Asynchronously traverses the DOM starting from a given root node and invokes a callback on each node. */
4 | export type traverseDom = (root: Element, callback: traverseRootCallback, nodeTypes: number[]) => Promise
5 |
6 | export interface DomTraverserDomModule {
7 | traverseDom: traverseDom
8 | }
9 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/evaluator.d.ts:
--------------------------------------------------------------------------------
1 | import type { Signal } from "./signal"
2 |
3 | export type evaluate = (string: string, content: Record, trackSignals?: boolean) => {
4 | result: any,
5 | detectedSignals: Signal[]
6 | }
7 |
8 | export interface EvaluatorModule {
9 | evaluate: evaluate
10 | }
11 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/event.d.ts:
--------------------------------------------------------------------------------
1 | export interface EventModule {
2 | on: on;
3 | off: off;
4 | customEventListener: customEventListener;
5 | customEvent: customEvent;
6 | dispatch: dispatch;
7 | }
8 |
9 | /** Event target types */
10 | export type CustomEventTarget = string | NodeListOf | Element[] | Element | Window;
11 |
12 | /** Custom event listener configuration */
13 | export interface CustomEventListenerConfig {
14 | /** Method to add an event listener. */
15 | on: CustomEventListenerOnHandler;
16 | /** Method to remove an event listener. */
17 | off?: (args: CustomEventListenerArgs) => void;
18 | /** Method for dispatching global event. */
19 | dispatch?: (args: CustomEvent) => void;
20 | }
21 |
22 | /** Custom event listener on handler type */
23 | export type CustomEventListenerOnHandler = (args: CustomEventListenerArgs) => void;
24 |
25 | /** Custom event listener arguments */
26 | export interface CustomEventListenerArgs {
27 | target: Element;
28 | listener: CallableFunction;
29 | options: AddEventListenerOptions;
30 | event: string;
31 | }
32 |
33 | /** Custom event listener type (extends for specific events) */
34 | export interface CustomEventListener extends EventListener { }
35 |
36 | export interface CustomEventListeners extends HTMLElementEventMap {
37 | clickOutside: CustomEventListener;
38 | remove: CustomEventListener;
39 | }
40 |
41 | /** Dispatch function type */
42 | export type dispatch = (eventName: string, eventData?: any, options?: Record) => boolean;
43 |
44 | /** Custom event creation function type */
45 | export type customEvent = (eventName: string, eventData?: any, options?: CustomEventInit) => CustomEvent;
46 |
47 | /** Custom event listener creation function type */
48 | export type customEventListener = (eventName: string, configOrHandler: CustomEventListenerConfig | CustomEventListenerOnHandler) => void;
49 |
50 | /** Plugin options type */
51 | export interface EventConfig {
52 | customEventListeners: Record;
53 | }
54 |
55 | /**
56 | * Add event listener to an element or for a custom event.
57 | * This method returns a prepared off listener.
58 | */
59 | export type on = (events: keyof CustomEventListeners, targetOrCallback: CustomEventTarget | CallableFunction, callbackOrOptions?: CallableFunction | AddEventListenerOptions, options?: AddEventListenerOptions) => CallableFunction;
60 |
61 | /**
62 | * Remove event listener from elements or from custom event.
63 | */
64 | export type off = (events: Extract void;
65 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/hyperscript.d.ts:
--------------------------------------------------------------------------------
1 | import type { Signal } from "./signal";
2 |
3 | export type HyperscriptChild = string | number | Element | Node | Signal;
4 |
5 | export interface HyperscriptChildAttrs {
6 | [key: string]: string | number | Signal;
7 | }
8 |
9 | /** Create element, bind Signals and Attributes. */
10 | export type h = (
11 | tagName: string,
12 | ...children: (HyperscriptChildAttrs | HyperscriptChild | HyperscriptChild[])[]
13 | ) => T;
14 |
15 | export interface HyperscriptModule {
16 | h: h
17 | }
18 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/intersection-observer.d.ts:
--------------------------------------------------------------------------------
1 | export type observeIntersection = (eleemnt: Element, callback: IntersectionObserverCallback, options?: IntersectionObserverInit) => IntersectionObserver
2 |
3 | export interface IntersectionObserverModule {
4 | observeIntersection: observeIntersection
5 | }
6 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/logger.d.ts:
--------------------------------------------------------------------------------
1 | export type Levels = 'log' | 'info' | 'warn' | 'error';
2 |
3 | /** Represents a log entry with information about the log type, message, and optional details. */
4 | export interface Log {
5 | /** The type of log entry (log, info, warn, error). */
6 | type: Levels;
7 | /** The log message. */
8 | message: string;
9 | /** The file associated with the log entry. */
10 | file?: string | null;
11 | /** The line number associated with the log entry. */
12 | lineNumber?: number | undefined;
13 | /** The column number associated with the log entry. */
14 | columnNumber?: number | undefined;
15 | /** The stack trace associated with the log entry. */
16 | stack?: string | null;
17 | }
18 |
19 | export interface LoggerConfig {
20 | levels?: Levels[];
21 | url: string;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/mutation-observer.d.ts:
--------------------------------------------------------------------------------
1 | /** Mutation observer listener type */
2 | export type mutationObserverListener = (mutationNodes: MutationNodes) => void;
3 |
4 | /** Optimized MutationRecords object */
5 | export interface MutationNodes {
6 | addedNodes: Node[];
7 | movedNodes: Node[];
8 | removedNodes: Node[];
9 | }
10 |
11 | /** Custom mutation observer creation function */
12 | export type createMutationObserver = (root: Element, listener: mutationObserverListener, options?: MutationObserverInit) => MutationObserver;
13 |
14 | /**
15 | * Observe dom mutations.
16 | * This function returns a callback that will remove the listener.
17 | */
18 | export type observeMutations = (listener: mutationObserverListener, options?: MutationObserverInit) => () => void;
19 |
20 | export interface MutationObserverModule {
21 | observeMutations: observeMutations;
22 | createMutationObserver: createMutationObserver;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/offset.d.ts:
--------------------------------------------------------------------------------
1 | interface OffsetInfo {
2 | top: number;
3 | bottom: number;
4 | left: number;
5 | right: number;
6 | }
7 |
8 | /** Returns info about element offset. */
9 | export type offset = (element: Element) => OffsetInfo
10 |
11 | export interface OffsetModule {
12 | offset: offset
13 | }
14 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/scope.d.ts:
--------------------------------------------------------------------------------
1 | import type { Signalize } from '../../src/Signalize';
2 |
3 | export interface ScopeConstrutorParams {
4 | node: Node
5 | }
6 |
7 | export type $cleanup = (data?: ScopeCleanupCallback) => void;
8 |
9 | export type $data = Record;
10 |
11 | /** Interface representing a scope with essential properties and methods. */
12 | export declare class Scope {
13 | constructor(config: ScopeConstrutorParams);
14 | /** Access to Signalize instance */
15 | $: Signalize;
16 | /** Root element associated with the scope */
17 | $el: Element;
18 | /** Optional parent scope (optional) */
19 | $parentScope?: Scope;
20 | /** Data associated with the scope */
21 | $data: Record;
22 | /** Cleanup function initializer */
23 | $cleanup: $cleanup;
24 | }
25 |
26 | /** Interface extending Node with a private property */
27 | export interface SignalizeNode extends Node {
28 | /** Internal reference to the scope (optional) */
29 | __signalizeScope?: Scope;
30 | }
31 |
32 | /** Type representing a function that initializes a scope. */
33 | export type ScopeInitFunction = (scope: Scope) => void;
34 |
35 | /** Type representing a function for scope cleanup. */
36 | export type ScopeCleanupCallback = () => void | Promise;
37 |
38 | /** Function type to create a scope. */
39 | export type scope = (
40 | node: SignalizeNode,
41 | init?: ScopeInitFunction
42 | ) => Scope | undefined;
43 |
44 | export interface ScopeModule {
45 | scope: scope
46 | }
47 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/signal.d.ts:
--------------------------------------------------------------------------------
1 | /** Callback for modifying value before setting watcher */
2 | export type BeforeSetSignalWatcher = (options: SignalWatcherArguments) => {
3 | value: T;
4 | settable?: boolean
5 | } | undefined;
6 |
7 | /** Callback for actions after setting watcher */
8 | export type AfterSetSignalWatcher = (options: SignalWatcherArguments) => void;
9 |
10 | export type OnGetSignalWatcher = (options: SignalWatcherArguments) => void;
11 |
12 | /** Available execution options for the watcher */
13 | export type SignalWatcherExecutionOption = 'beforeSet' | 'afterSet' | 'onGet';
14 |
15 | /** Options for configuring a signal watcher */
16 | export interface SignalWatcherOptions {
17 | /** If true, watcher is executed immediately */
18 | immediate?: boolean;
19 | /** Execution mode. */
20 | execution?: SignalWatcherExecutionOption;
21 | }
22 |
23 | /** Arguments passed to signal watcher function */
24 | export interface SignalWatcherArguments {
25 | /** New value */
26 | newValue?: T; // New value being set
27 | /** Previously setted value */
28 | oldValue?: T;
29 | }
30 |
31 | /** Function to stop watching a signal */
32 | export type SignalUnwatch = () => void;
33 |
34 | /** Collection of categorized signal watcher functions */
35 | export interface SignalWatchers {
36 | beforeSet: Set;
37 | afterSet: Set;
38 | onGet: Set;
39 | }
40 |
41 | export type SetSignalWatcher = (listener: BeforeSetSignalWatcher | AfterSetSignalWatcher | OnGetSignalWatcher, options?: SignalWatcherOptions) => SignalUnwatch;
42 |
43 | /** Represents a signal with a specific value and associated watchers. */
44 | export declare class Signal {
45 | constructor(value: T);
46 | value: T;
47 | watchers: SignalWatchers;
48 | /** Adds a watcher function to the signal. */
49 | watch: SetSignalWatcher;
50 | toString: () => string;
51 | valueOf: () => T;
52 | toJSON: () => T;
53 | }
54 |
55 | /** Creates a new Signal instance with the provided default value. */
56 | export type signal = (defaultValue: T) => Signal;
57 |
58 | export interface SignalModule {
59 | signal: signal,
60 | Signal: Signal
61 | }
62 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/sizes.d.ts:
--------------------------------------------------------------------------------
1 | export type width = (element: Element|Document) => number;
2 | export type height = (element: Element|Document) => number;
3 |
4 | export interface SizesModule {
5 | height: height;
6 | width: width;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/snippets.d.ts:
--------------------------------------------------------------------------------
1 | export interface SnippetOptions {
2 | snippetActions?: string[];
3 | }
4 |
5 | export interface RedrawSnippetOptions {
6 | snippets?: Record;
7 | /** Enable/Disable View Transitions */
8 | transitionEnabled?: boolean;
9 | }
10 |
11 | /** Sync elements in dom with snippet="" with the new content- */
12 | export type redrawSnippet = (content: string, options?: RedrawSnippetOptions) => Promise;
13 |
14 | export interface SnippetsModule {
15 | redrawSnippet: redrawSnippet
16 | }
17 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/spa.d.ts:
--------------------------------------------------------------------------------
1 | /** Represents an action type for managing state (e.g., push or replace). */
2 | export type StateAction = 'push' | 'replace';
3 |
4 | export interface NavigationData {
5 | httpMethod?: string|null
6 | /** The URL for navigation. */
7 | url: string|URL;
8 | /** The scroll position on the X-axis. */
9 | scrollX?: number;
10 | /** The scroll position on the |-axis. */
11 | scrollY?: number;
12 | /** The action type for managing state (push or replace). */
13 | stateAction: StateAction;
14 | }
15 |
16 | /** Represents data associated with dispatching events for Single Page Application (SPA). */
17 | export interface NavigationEventData {
18 | /** Error that occured during navigation. */
19 | error: unknown|null;
20 | /** The URL for navigation. */
21 | url: URL|string;
22 | /** The scroll position on the X-axis. */
23 | scrollX: number;
24 | /** The scroll position on the Y-axis. */
25 | scrollY: number;
26 | /** The action type for managing state (push or replace). */
27 | stateAction: StateAction;
28 | }
29 |
30 | export type ResponseCache = Record;
31 |
32 | /** Spa modulee options. */
33 | export interface SpaConfig {
34 | /** The response cache header name. Used to enable/disable cache. */
35 | cacheHeader?: string;
36 | /** The app version header name. Used to dispatch event, that SPA version has changed. */
37 | appVersionHeader?: string;
38 | }
39 |
40 | export interface HistoryState {
41 | httpMethod?: string
42 | /** Current state url. */
43 | url: string;
44 | /** If the state is triggered by spa */
45 | spa: boolean;
46 | /** Scroll X position of current state. */
47 | scrollX: number;
48 | /** Scroll Y position of current state. */
49 | scrollY: number;
50 | }
51 |
52 | export type navigate = (data: NavigationData) => Promise
53 |
54 | export interface SpaModule {
55 | navigate: navigate
56 | }
57 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/strings/cases.d.ts:
--------------------------------------------------------------------------------
1 | export type dashCase = (string: string) => string;
2 |
3 | export interface StringsCasesModule {
4 | dashCase: dashCase
5 | }
6 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/task.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A small helper for breaking long running tasks.
3 | *
4 | * https://nitropack.io/blog/post/improve-interaction-to-next-paint-inp
5 | * https://web.dev/optimize-long-tasks/
6 | */
7 | export type task = (callback: CallableFunction) => void;
8 |
9 | export interface TaskModule {
10 | task: task
11 | }
12 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/viewport.d.ts:
--------------------------------------------------------------------------------
1 | export interface IsInViewportInfo {
2 | top: boolean;
3 | bottom: boolean;
4 | whole: boolean;
5 | }
6 |
7 | /** Information about the visibility of an element within the viewport. */
8 | export type isInViewport = (element: Element) => IsInViewportInfo
9 |
10 | export interface ViewportModule {
11 | isInViewport: isInViewport
12 | }
13 |
--------------------------------------------------------------------------------
/packages/signalizejs/types/modules/visibility.ts:
--------------------------------------------------------------------------------
1 | export type isVisible = (element: HTMLElement) => boolean;
2 |
3 | export interface VisibilityModule {
4 | isVisible: isVisible
5 | }
6 |
--------------------------------------------------------------------------------
/playwright.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 | import { env } from 'process';
3 |
4 | const isDev = env.DEV === 'true';
5 | const devServerUrl = 'http://0.0.0.0:4000';
6 | const packagesDir = 'packages';
7 |
8 | let projects = [
9 | {
10 | name: 'chromium',
11 | use: { ...devices['Desktop Chrome'] }
12 | }
13 | ];
14 |
15 | if (!isDev) {
16 | projects = [
17 | ...projects,
18 | {
19 | name: 'firefox',
20 | use: { ...devices['Desktop Firefox'] }
21 | },
22 | {
23 | name: 'webkit',
24 | use: { ...devices['Desktop Safari'] },
25 | }
26 | ];
27 | }
28 |
29 | export default defineConfig({
30 | testDir: `./${packagesDir}/signalizejs/tests`,
31 | fullyParallel: true,
32 | forbidOnly: !isDev,
33 | retries: isDev ? 2 : 0,
34 | workers: isDev ? 1 : undefined,
35 | reporter: 'line',
36 |
37 | use: {
38 | trace: 'on-first-retry',
39 | baseURL: `${devServerUrl}/`
40 | },
41 |
42 | projects,
43 | webServer: {
44 | command: 'npm http-server ./packages -p 4000',
45 | url: devServerUrl,
46 | reuseExistingServer: !isDev,
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "target": "ESNext",
5 | "newLine": "LF",
6 | "checkJs": true,
7 | "strict": true
8 | },
9 | "exclude": [
10 | "node_modules"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------