mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Merge pull request #659 from bunkerity/dev
Merge branch "dev" into branch "staging"
This commit is contained in:
commit
18e5f7bff6
35 changed files with 5280 additions and 4335 deletions
5
.github/codeql.yml
vendored
5
.github/codeql.yml
vendored
|
|
@ -6,5 +6,8 @@ paths:
|
|||
- src/ui
|
||||
- src/common
|
||||
paths-ignore:
|
||||
- src/ui/static
|
||||
- src/ui/static/js/tsparticles.bundle.min.js
|
||||
- src/ui/static/js/editor
|
||||
- src/ui/static/js/utils/flatpickr.js
|
||||
- src/ui/static/js/utils/purify
|
||||
- src/common/core/modsecurity/files
|
||||
|
|
|
|||
31
.github/workflows/codeql.yml
vendored
Normal file
31
.github/workflows/codeql.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
name: CodeQL Analysis
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Weekly on Saturdays.
|
||||
- cron: "30 1 * * 6"
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
code-security:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["python", "javascript"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql.yml
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
27
.github/workflows/dev.yml
vendored
27
.github/workflows/dev.yml
vendored
|
|
@ -63,45 +63,28 @@ jobs:
|
|||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
# Python code security
|
||||
code-security:
|
||||
runs-on: ubuntu-latest
|
||||
codeql:
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["python"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql.yml
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
# UI tests
|
||||
tests-ui:
|
||||
needs: [code-security, build-containers]
|
||||
needs: [codeql, build-containers]
|
||||
uses: ./.github/workflows/tests-ui.yml
|
||||
with:
|
||||
RELEASE: dev
|
||||
tests-ui-linux:
|
||||
needs: [code-security, build-packages]
|
||||
needs: [codeql, build-packages]
|
||||
uses: ./.github/workflows/tests-ui-linux.yml
|
||||
with:
|
||||
RELEASE: dev
|
||||
|
||||
# Core tests
|
||||
prepare-tests-core:
|
||||
needs: [code-security, build-containers, build-packages]
|
||||
needs: [codeql, build-containers, build-packages]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
|
|
|||
4
.github/workflows/linux-build.yml
vendored
4
.github/workflows/linux-build.yml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Replace VERSION
|
||||
if: inputs.RELEASE == 'testing' || inputs.RELEASE == 'dev'
|
||||
if: inputs.RELEASE == 'testing' || inputs.RELEASE == 'dev' || inputs.RELEASE == 'ui'
|
||||
run: ./misc/update-version.sh ${{ inputs.RELEASE }}
|
||||
- name: Extract arch
|
||||
run: |
|
||||
|
|
@ -91,7 +91,7 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Build testing package image
|
||||
- name: Build package image
|
||||
if: inputs.RELEASE == 'testing' || inputs.RELEASE == 'dev'
|
||||
if: inputs.RELEASE == 'testing' || inputs.RELEASE == 'dev' || inputs.RELEASE == 'ui'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
|
|
|
|||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -7,6 +7,16 @@ on:
|
|||
branches: [master]
|
||||
|
||||
jobs:
|
||||
scorecards-analysis:
|
||||
uses: ./.github/workflows/scorecards-analysis.yml
|
||||
|
||||
codeql:
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
# Build amd64 + 386 containers images
|
||||
build-containers:
|
||||
strategy:
|
||||
|
|
@ -123,7 +133,7 @@ jobs:
|
|||
# Wait for all builds and extract VERSION
|
||||
wait-builds:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-containers, build-containers-arm, build-packages]
|
||||
needs: [codeql, build-containers, build-containers-arm, build-packages]
|
||||
outputs:
|
||||
version: ${{ steps.getversion.outputs.version }}
|
||||
versionrpm: ${{ steps.getversionrpm.outputs.versionrpm }}
|
||||
|
|
|
|||
30
.github/workflows/scorecards-analysis.yml
vendored
Normal file
30
.github/workflows/scorecards-analysis.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: Scorecard analysis workflow
|
||||
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
# Weekly on Saturdays.
|
||||
- cron: "30 1 * * 6"
|
||||
workflow_call:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@v2.2.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
- name: "Upload SARIF results to code scanning"
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
29
.github/workflows/staging.yml
vendored
29
.github/workflows/staging.yml
vendored
|
|
@ -64,33 +64,16 @@ jobs:
|
|||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
# Python code security
|
||||
code-security:
|
||||
runs-on: ubuntu-latest
|
||||
codeql:
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["python"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql.yml
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
# Create infrastructures and prepare tests
|
||||
create-infras:
|
||||
needs: [code-security, build-containers, build-packages]
|
||||
needs: [codeql, build-containers, build-packages]
|
||||
strategy:
|
||||
matrix:
|
||||
type: [docker, autoconf, swarm, k8s, linux]
|
||||
|
|
@ -102,7 +85,7 @@ jobs:
|
|||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
K8S_IP: ${{ secrets.K8S_IP }}
|
||||
prepare-tests-core:
|
||||
needs: [code-security, build-containers, build-packages]
|
||||
needs: [codeql, build-containers, build-packages]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
|
@ -116,12 +99,12 @@ jobs:
|
|||
|
||||
# Perform tests
|
||||
tests-ui:
|
||||
needs: [code-security, build-containers]
|
||||
needs: [codeql, build-containers]
|
||||
uses: ./.github/workflows/tests-ui.yml
|
||||
with:
|
||||
RELEASE: testing
|
||||
tests-ui-linux:
|
||||
needs: [code-security, build-packages]
|
||||
needs: [codeql, build-packages]
|
||||
uses: ./.github/workflows/tests-ui-linux.yml
|
||||
with:
|
||||
RELEASE: testing
|
||||
|
|
|
|||
2
.github/workflows/tests-ui-linux.yml
vendored
2
.github/workflows/tests-ui-linux.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
sudo apt update
|
||||
sudo apt install -y nginx=1.24.0-1~jammy
|
||||
- name: Fix version without a starting number
|
||||
if: inputs.RELEASE == 'testing' || inputs.RELEASE == 'dev'
|
||||
if: inputs.RELEASE == 'testing' || inputs.RELEASE == 'dev' || inputs.RELEASE == 'ui'
|
||||
run: echo "force-bad-version" | sudo tee -a /etc/dpkg/dpkg.cfg
|
||||
- name: Install BunkerWeb
|
||||
run: sudo apt install -fy /tmp/bunkerweb.deb
|
||||
|
|
|
|||
36
.github/workflows/ui.yml
vendored
36
.github/workflows/ui.yml
vendored
|
|
@ -33,9 +33,43 @@ jobs:
|
|||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
# Build Linux packages
|
||||
build-packages:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
linux: [ubuntu]
|
||||
include:
|
||||
- linux: ubuntu
|
||||
package: deb
|
||||
uses: ./.github/workflows/linux-build.yml
|
||||
with:
|
||||
RELEASE: ui
|
||||
LINUX: ${{ matrix.linux }}
|
||||
PACKAGE: ${{ matrix.package }}
|
||||
TEST: true
|
||||
PLATFORMS: linux/amd64
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
codeql:
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
# UI tests
|
||||
tests-ui:
|
||||
needs: [build-containers]
|
||||
needs: [codeql, build-containers]
|
||||
uses: ./.github/workflows/tests-ui.yml
|
||||
with:
|
||||
RELEASE: ui
|
||||
tests-ui-linux:
|
||||
needs: [codeql, build-packages]
|
||||
uses: ./.github/workflows/tests-ui-linux.yml
|
||||
with:
|
||||
RELEASE: ui
|
||||
|
|
|
|||
|
|
@ -10,5 +10,10 @@ CONTRIBUTING.md
|
|||
LICENSE.md
|
||||
README.md
|
||||
SECURITY.md
|
||||
src/ui/static
|
||||
examples/*
|
||||
tsparticles.bundle.min.js
|
||||
flatpickr.*
|
||||
src/ui/static/js/editor/*
|
||||
src/ui/static/js/utils/purify/*
|
||||
src/ui/templates/*
|
||||
datepicker-foundation.css
|
||||
examples/*
|
||||
|
|
|
|||
45
README.md
45
README.md
|
|
@ -1,15 +1,20 @@
|
|||
<p align="center">
|
||||
<img alt="BunkerWeb logo" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/misc/logo.png" />
|
||||
<img alt="BunkerWeb logo" src="./misc/logo.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/github/v/release/bunkerity/bunkerweb?label=stable" />
|
||||
<img src="https://img.shields.io/github/v/release/bunkerity/bunkerweb?include_prereleases&label=latest" />
|
||||
<br />
|
||||
<img src="https://img.shields.io/github/last-commit/bunkerity/bunkerweb" />
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/bunkerity/bunkerweb/dev.yml?branch=dev&label=CI%2FCD%20dev" />
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/bunkerity/bunkerweb/staging.yml?branch=staging&label=CI%2FCD%20staging" />
|
||||
<br />
|
||||
<img src="https://img.shields.io/github/issues/bunkerity/bunkerweb">
|
||||
<img src="https://img.shields.io/github/issues-pr/bunkerity/bunkerweb">
|
||||
<a href="https://securityscorecards.dev/viewer/?uri=github.com/bunkerity/bunkerweb">
|
||||
<img src="https://api.securityscorecards.dev/projects/github.com/bunkerity/bunkerweb/badge" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
|
@ -17,7 +22,7 @@
|
|||
|
|
||||
👨💻 <a href="https://demo.bunkerweb.io">Demo</a>
|
||||
|
|
||||
🛡️ <a href="https://github.com/bunkerity/bunkerweb/tree/v1.5.2/examples">Examples</a>
|
||||
🛡️ <a href="./examples">Examples</a>
|
||||
|
|
||||
💬 <a href="https://discord.com/invite/fTf46FmtyD">Chat</a>
|
||||
|
|
||||
|
|
@ -33,7 +38,7 @@
|
|||
# BunkerWeb
|
||||
|
||||
<p align="center">
|
||||
<img alt="overview" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/intro-overview.svg" />
|
||||
<img alt="Overview banner" src="./docs/assets/img/intro-overview.svg" />
|
||||
</p>
|
||||
|
||||
BunkerWeb is a next-generation and open-source Web Application Firewall (WAF).
|
||||
|
|
@ -77,7 +82,7 @@ A demo website protected with BunkerWeb is available at [demo.bunkerweb.io](http
|
|||
# Concepts
|
||||
|
||||
<p align="center">
|
||||
<img alt="BunkerWeb logo" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/concepts.svg" />
|
||||
<img alt="Concepts banner" src="./docs/assets/img/concepts.svg" />
|
||||
</p>
|
||||
|
||||
You will find more information about the key concepts of BunkerWeb in the [documentation](https://docs.bunkerweb.io/1.5.2/concepts).
|
||||
|
|
@ -160,7 +165,7 @@ In other words, the scheduler is the brain of BunkerWeb.
|
|||
## Docker
|
||||
|
||||
<p align="center">
|
||||
<img alt="Docker" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/integration-docker.svg" />
|
||||
<img alt="Docker banner" src="./docs/assets/img/integration-docker.svg" />
|
||||
</p>
|
||||
|
||||
We provide ready to use prebuilt images for x64, x86, armv7 and arm64 platforms on [Docker Hub](https://hub.docker.com/u/bunkerity).
|
||||
|
|
@ -176,7 +181,7 @@ You will find more information in the [Docker integration section](https://docs.
|
|||
## Docker autoconf
|
||||
|
||||
<p align="center">
|
||||
<img alt="Docker autoconf" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/integration-autoconf.svg" />
|
||||
<img alt="Docker autoconf banner" src="./docs/assets/img/integration-autoconf.svg" />
|
||||
</p>
|
||||
|
||||
The downside of using environment variables is that the container needs to be recreated each time there is an update which is not very convenient. To counter that issue, you can use another image called **autoconf** which will listen for Docker events and automatically reconfigure BunkerWeb in real-time without recreating the container.
|
||||
|
|
@ -188,7 +193,7 @@ You will find more information in the [Docker autoconf section](https://docs.bun
|
|||
## Swarm
|
||||
|
||||
<p align="center">
|
||||
<img alt="Swarm" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/integration-swarm.svg" />
|
||||
<img alt="Swarm banner" src="./docs/assets/img/integration-swarm.svg" />
|
||||
</p>
|
||||
|
||||
To automatically configure BunkerWeb instances, a special service, called **autoconf** will listen for Docker Swarm events like service creation or deletion and automatically configure the **BunkerWeb instances** in real-time without downtime.
|
||||
|
|
@ -200,7 +205,7 @@ You will find more information in the [Swarm section](https://docs.bunkerweb.io/
|
|||
## Kubernetes
|
||||
|
||||
<p align="center">
|
||||
<img alt="Kubernetes" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/integration-kubernetes.svg" />
|
||||
<img alt="Kubernetes banner" src="./docs/assets/img/integration-kubernetes.svg" />
|
||||
</p>
|
||||
|
||||
The autoconf acts as an [Ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) and will configure the BunkerWeb instances according to the [Ingress resources](https://kubernetes.io/docs/concepts/services-networking/ingress/). It also monitors other Kubernetes objects like [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) for custom configurations.
|
||||
|
|
@ -210,7 +215,7 @@ You will find more information in the [Kubernetes section](https://docs.bunkerwe
|
|||
## Linux
|
||||
|
||||
<p align="center">
|
||||
<img alt="Linux" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/integration-linux.svg" />
|
||||
<img alt="Linux banner" src="./docs/assets/img/integration-linux.svg" />
|
||||
</p>
|
||||
|
||||
List of supported Linux distros :
|
||||
|
|
@ -227,7 +232,7 @@ You will find more information in the [Linux section](https://docs.bunkerweb.io/
|
|||
## Ansible
|
||||
|
||||
<p align="center">
|
||||
<img alt="Ansible" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.2/docs/assets/img/integration-ansible.svg" />
|
||||
<img alt="Ansible banner" src="./docs/assets/img/integration-ansible.svg" />
|
||||
</p>
|
||||
|
||||
List of supported Linux distros :
|
||||
|
|
@ -282,7 +287,7 @@ Check the [settings section](https://docs.bunkerweb.io/1.5.2/settings) of the do
|
|||
# Web UI
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=Ao20SfvQyr4" target="_blank"><img alt="BunkerWeb UI demo" src="https://yt-embed.herokuapp.com/embed?v=Ao20SfvQyr4" /></a>
|
||||
<iframe style="display: block;" width="560" height="315" alt="BunkerWeb UI demo" src="https://www.youtube-nocookie.com/embed/Ao20SfvQyr4" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</p>
|
||||
|
||||
The "Web UI" is a web application that helps you manage your BunkerWeb instance using a user-friendly interface instead of the command-line one.
|
||||
|
|
@ -303,15 +308,15 @@ BunkerWeb comes with a plugin system to make it possible to easily add new featu
|
|||
|
||||
Here is the list of "official" plugins that we maintain (see the [bunkerweb-plugins](https://github.com/bunkerity/bunkerweb-plugins) repository for more information) :
|
||||
|
||||
| Name | Version | Description | Link |
|
||||
| :------------: | :-----: | :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------: |
|
||||
| Name | Version | Description | Link |
|
||||
| :------------: | :-----: | :------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------: |
|
||||
| **ClamAV** | 1.1 | Automatically scans uploaded files with the ClamAV antivirus engine and denies the request when a file is detected as malicious. | [bunkerweb-plugins/clamav](https://github.com/bunkerity/bunkerweb-plugins/tree/main/clamav) |
|
||||
| **Coraza** | 1.1 | Inspect requests using a the Coraza WAF (alternative of ModSecurity). | [bunkerweb-plugins/coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) |
|
||||
| **Coraza** | 1.1 | Inspect requests using a the Coraza WAF (alternative of ModSecurity). | [bunkerweb-plugins/coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) |
|
||||
| **CrowdSec** | 1.1 | CrowdSec bouncer for BunkerWeb. | [bunkerweb-plugins/crowdsec](https://github.com/bunkerity/bunkerweb-plugins/tree/main/crowdsec) |
|
||||
| **Discord** | 1.1 | Send security notifications to a Discord channel using a Webhook. | [bunkerweb-plugins/discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) |
|
||||
| **Slack** | 1.1 | Send security notifications to a Slack channel using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) |
|
||||
| **Discord** | 1.1 | Send security notifications to a Discord channel using a Webhook. | [bunkerweb-plugins/discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) |
|
||||
| **Slack** | 1.1 | Send security notifications to a Slack channel using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) |
|
||||
| **VirusTotal** | 1.1 | Automatically scans uploaded files with the VirusTotal API and denies the request when a file is detected as malicious. | [bunkerweb-plugins/virustotal](https://github.com/bunkerity/bunkerweb-plugins/tree/main/virustotal) |
|
||||
| **WebHook** | 1.1 | Send security notifications to a custom HTTP endpoint using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) |
|
||||
| **WebHook** | 1.1 | Send security notifications to a custom HTTP endpoint using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) |
|
||||
|
||||
You will find more information in the [plugins section](https://docs.bunkerweb.io/1.5.2/plugins) of the documentation.
|
||||
|
||||
|
|
@ -341,12 +346,12 @@ Please don't use [GitHub issues](https://github.com/bunkerity/bunkerweb/issues)
|
|||
|
||||
# License
|
||||
|
||||
This project is licensed under the terms of the [GNU Affero General Public License (AGPL) version 3](https://github.com/bunkerity/bunkerweb/tree/1.5.2/LICENSE.md).
|
||||
This project is licensed under the terms of the [GNU Affero General Public License (AGPL) version 3](./LICENSE.md).
|
||||
|
||||
# Contribute
|
||||
|
||||
If you would like to contribute to the plugins you can read the [contributing guidelines](https://github.com/bunkerity/bunkerweb/tree/1.5.2/CONTRIBUTING.md) to get started.
|
||||
If you would like to contribute to the plugins you can read the [contributing guidelines](./CONTRIBUTING.md) to get started.
|
||||
|
||||
# Security policy
|
||||
|
||||
We take security bugs as serious issues and encourage responsible disclosure, see our [security policy](https://github.com/bunkerity/bunkerweb/tree/1.5.2/SECURITY.md) for more information.
|
||||
We take security bugs as serious issues and encourage responsible disclosure, see our [security policy](./SECURITY.md) for more information.
|
||||
|
|
|
|||
|
|
@ -2590,15 +2590,34 @@ h6 {
|
|||
}
|
||||
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
|
@ -3310,15 +3329,34 @@ h6 {
|
|||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 700;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
|
||||
|
|
@ -3343,15 +3381,34 @@ h6 {
|
|||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(94 114 228 / var(--tw-bg-opacity));
|
||||
padding: 0.75rem;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
|
@ -3370,15 +3427,34 @@ h6 {
|
|||
font-weight: 700;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
|
||||
|
|
@ -3539,15 +3615,34 @@ h6 {
|
|||
padding-right: 0.5rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(8 85 119 / var(--tw-text-opacity));
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
|
||||
|
|
@ -3569,15 +3664,34 @@ h6 {
|
|||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(94 114 228 / var(--tw-bg-opacity));
|
||||
padding: 0.75rem;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
|
@ -3666,15 +3780,34 @@ h6 {
|
|||
.settings-tabs-mobile-btn-text {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(8 85 119 / var(--tw-text-opacity));
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
|
||||
backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
text-decoration-color,
|
||||
fill,
|
||||
stroke,
|
||||
opacity,
|
||||
box-shadow,
|
||||
transform,
|
||||
filter,
|
||||
backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
.datepicker{display:none}.datepicker.active{display:block}.datepicker-dropdown{left:0;padding-top:4px;position:absolute;top:0;z-index:10}.datepicker-dropdown.datepicker-orient-top{padding-bottom:4px;padding-top:0}.datepicker-picker{background-color:#fefefe;border-radius:0;display:inline-block}.datepicker-dropdown .datepicker-picker{box-shadow:0 0 0 1px #cacaca}.datepicker-picker span{-webkit-touch-callout:none;border:0;border-radius:0;cursor:default;display:block;flex:1;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.datepicker-main{padding:2px}.datepicker-footer{background-color:#e6e6e6;box-shadow:inset 0 1px 1px hsla(0,0%,4%,.1)}.datepicker-controls,.datepicker-grid,.datepicker-view,.datepicker-view .days-of-week{display:flex}.datepicker-grid{flex-wrap:wrap}.datepicker-view .days .datepicker-cell,.datepicker-view .dow{flex-basis:14.2857142857%}.datepicker-view.datepicker-grid .datepicker-cell{flex-basis:25%}.datepicker-cell,.datepicker-view .week{height:2.25rem;line-height:2.25rem}.datepicker-title{background-color:#e6e6e6;box-shadow:inset 0 -1px 1px hsla(0,0%,4%,.1);font-weight:700;padding:.375rem .75rem;text-align:center}.datepicker-header .datepicker-controls{padding:2px 2px 0}.datepicker-controls .button{background-color:#fefefe;color:#0a0a0a;margin:0}.datepicker-controls .button:focus,.datepicker-controls .button:hover{background-color:#d8d8d8}.datepicker-controls .button:focus[disabled],.datepicker-controls .button:hover[disabled]{background-color:#fefefe;color:#0a0a0a;opacity:.25}.datepicker-header .datepicker-controls .button{border-color:transparent;font-weight:700}.datepicker-footer .datepicker-controls .button{border-radius:0;font-size:.75rem;margin:calc(.375rem - 1px) .375rem;width:100%}.datepicker-controls .view-switch{flex:auto}.datepicker-controls .next-btn,.datepicker-controls .prev-btn{padding-left:.375rem;padding-right:.375rem;width:2.25rem}.datepicker-controls .next-btn.disabled,.datepicker-controls .prev-btn.disabled{visibility:hidden}.datepicker-view .dow{font-size:.875rem;font-weight:700;height:1.5rem;line-height:1.5rem}.datepicker-view .week{color:#8a8a8a;font-size:.75rem;width:2.25rem}@media (max-width:22.5rem){.datepicker-view .week{width:1.96875rem}}.datepicker-grid{width:15.75rem}@media (max-width:22.5rem){.calendar-weeks+.days .datepicker-grid{width:13.78125rem}}.datepicker-cell:not(.disabled):hover{background-color:#f8f8f8;cursor:pointer}.datepicker-cell.focused:not(.selected){background-color:#f1f1f1}.datepicker-cell.selected,.datepicker-cell.selected:hover{background-color:#1779ba;color:#fefefe;font-weight:semibold}.datepicker-cell.disabled{color:#e6e6e6}.datepicker-cell.next:not(.disabled),.datepicker-cell.prev:not(.disabled){color:#cacaca}.datepicker-cell.next.selected,.datepicker-cell.prev.selected{color:#e5e5e5}.datepicker-cell.highlighted:not(.selected):not(.range):not(.today){background-color:#f7f7f7;border-radius:0}.datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused,.datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover{background-color:#f1f1f1}.datepicker-cell.today:not(.selected){background-color:#d7ecfa}.datepicker-cell.today:not(.selected):not(.disabled){color:#8a8a8a}.datepicker-cell.today.focused:not(.selected){background-color:#cbe7f9}.datepicker-cell.range-end:not(.selected),.datepicker-cell.range-start:not(.selected){background-color:#767676;color:#fefefe}.datepicker-cell.range-end.focused:not(.selected),.datepicker-cell.range-start.focused:not(.selected){background-color:#707070}.datepicker-cell.range-end,.datepicker-cell.range-start{border-radius:0 0 0 0}.datepicker-cell.range{background-color:#e6e6e6;border-radius:0}.datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover{background-color:#e0e0e0}.datepicker-cell.range.disabled{color:#cdcdcd}.datepicker-cell.range.focused{background-color:#d9d9d9}.datepicker-cell.range.today{background-color:#b3dbf6}.datepicker-view.datepicker-grid .datepicker-cell{height:4.5rem;line-height:4.5rem}.datepicker-input.in-edit{border-color:#a4a4a4}.datepicker-input.in-edit:active,.datepicker-input.in-edit:focus{box-shadow:0 0 .25em .25em hsla(0,0%,64%,.2)}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -16,7 +16,9 @@ class Download {
|
|||
this.listContainer.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute(`data-${this.prefix}-download`)
|
||||
e.target
|
||||
.closest("button")
|
||||
.hasAttribute(`data-${this.prefix}-download`)
|
||||
) {
|
||||
const btnEl = e.target.closest("button");
|
||||
const jobName = btnEl.getAttribute("data-cache-download");
|
||||
|
|
@ -31,8 +33,8 @@ class Download {
|
|||
window.open(
|
||||
`${location.href.replace(
|
||||
"cache",
|
||||
"jobs"
|
||||
)}/download?job_name=${jobName}&file_name=${fileName}`
|
||||
"jobs",
|
||||
)}/download?job_name=${jobName}&file_name=${fileName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import {
|
||||
FolderNav,
|
||||
FolderEditor,
|
||||
FolderModal,
|
||||
FolderDropdown,
|
||||
} from "./utils/file.manager.js";
|
||||
|
||||
const setModal = new FolderModal("configs");
|
||||
const setEditor = new FolderEditor();
|
||||
const setFolderNav = new FolderNav("configs");
|
||||
const setDropdown = new FolderDropdown("configs");
|
||||
import {
|
||||
FolderNav,
|
||||
FolderEditor,
|
||||
FolderModal,
|
||||
FolderDropdown,
|
||||
} from "./utils/file.manager.js";
|
||||
|
||||
const setModal = new FolderModal("configs");
|
||||
const setEditor = new FolderEditor();
|
||||
const setFolderNav = new FolderNav("configs");
|
||||
const setDropdown = new FolderDropdown("configs");
|
||||
|
|
|
|||
|
|
@ -1,317 +1,318 @@
|
|||
import { Checkbox, Select, Password, DisabledPop } from "./utils/form.js";
|
||||
|
||||
class Menu {
|
||||
constructor() {
|
||||
this.sidebarEl = document.querySelector("[data-sidebar-menu]");
|
||||
this.toggleBtn = document.querySelector("[data-sidebar-menu-toggle]");
|
||||
this.closeBtn = document.querySelector("[data-sidebar-menu-close]");
|
||||
|
||||
this.toggleBtn.addEventListener("click", this.toggle.bind(this));
|
||||
this.closeBtn.addEventListener("click", this.close.bind(this));
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("aside").hasAttribute("data-sidebar-menu") &&
|
||||
e.target.closest("button").getAttribute("role") === "tab"
|
||||
) {
|
||||
this.close();
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.sidebarEl.classList.toggle("-translate-x-full");
|
||||
}
|
||||
|
||||
close() {
|
||||
this.sidebarEl.classList.add("-translate-x-full");
|
||||
}
|
||||
}
|
||||
|
||||
class News {
|
||||
constructor() {
|
||||
this.BASE_URL = "https://www.bunkerweb.io/";
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("load", async () => {
|
||||
try {
|
||||
const res = await fetch("https://www.bunkerweb.io/api/posts/0/2", {
|
||||
headers: {
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
return await this.render(res);
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
render(lastNews) {
|
||||
const newsContainer = document.querySelector("[data-news-container]");
|
||||
//remove default message
|
||||
newsContainer.textContent = "";
|
||||
//render last news
|
||||
lastNews.forEach((news) => {
|
||||
//get info
|
||||
const slug = news.slug;
|
||||
const img = news.photo.url;
|
||||
const excerpt = news.excerpt;
|
||||
const tags = news.tags;
|
||||
const date = news.date;
|
||||
const lastUpdate = news.lastUpdate;
|
||||
//create html card from infos
|
||||
const cardHTML = this.template(
|
||||
slug,
|
||||
img,
|
||||
excerpt,
|
||||
tags,
|
||||
date,
|
||||
lastUpdate
|
||||
);
|
||||
//add to DOM
|
||||
document
|
||||
.querySelector("[data-news-container]")
|
||||
.insertAdjacentHTML("afterbegin", cardHTML);
|
||||
});
|
||||
}
|
||||
|
||||
template(slug, img, excerpt, tags, date, lastUpdate) {
|
||||
//loop on tags to get list
|
||||
let tagList = "";
|
||||
tags.forEach((tag) => {
|
||||
tagList += ` <a
|
||||
href="${this.BASE_URL}/blog/tag/${tag.slug}"
|
||||
class="my-0 mr-1 rounded bg-secondary hover:brightness-90 hover:-translate-y-0.4 text-white py-1 px-2 text-sm"
|
||||
>
|
||||
${tag.name}
|
||||
</a>`;
|
||||
});
|
||||
//create card
|
||||
const card = `
|
||||
<div
|
||||
class="min-h-[400px] w-full col-span-12 transition hover:-translate-y-2 bg-gray-100 dark:bg-slate-900 rounded px-6 py-4 m-2 flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<img role="link"
|
||||
onclick="window.location.href='${this.BASE_URL}/blog/post/${slug}'"
|
||||
class="cursor-pointer rounded w-full h-40 m-0 object-cover"
|
||||
src="${img}"
|
||||
alt="image"
|
||||
/>
|
||||
<h3 role="link"
|
||||
onclick="window.location.href='${this.BASE_URL}/blog/post/${slug}'"
|
||||
class="cursor-pointer mt-3 mb-1 text-3xl dark:text-white tracking-wide">{{ post['title'] }}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div role="link"
|
||||
onclick="window.location.href='${this.BASE_URL}/blog/post/${slug}'"
|
||||
class="cursor-pointer min-h-[130px] mb-3 text-lg dark:text-gray-300 text-gray-600 pt-3">
|
||||
${excerpt}
|
||||
</div>
|
||||
<div class="min-h-[75px] mt-2 flex flex-wrap justify-start items-end align-bottom">
|
||||
${tagList}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col justify-start items-start">
|
||||
<span class="text-xs dark:text-gray-300 text-gray-600"
|
||||
>Posted on : ${date}</span
|
||||
>
|
||||
{% if post["updatedAt"] %}
|
||||
<span class="text-xs dark:text-gray-300 text-gray-600"
|
||||
>Last update : ${lastUpdate}</span
|
||||
>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
</div> `;
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
class Sidebar {
|
||||
constructor(elAtt, btnOpenAtt, btnCloseAtt) {
|
||||
this.sidebarEl = document.querySelector(elAtt);
|
||||
this.openBtn = document.querySelector(btnOpenAtt);
|
||||
this.closeBtn = document.querySelector(btnCloseAtt);
|
||||
this.openBtn.addEventListener("click", this.open.bind(this));
|
||||
this.closeBtn.addEventListener("click", this.close.bind(this));
|
||||
}
|
||||
|
||||
open() {
|
||||
this.sidebarEl.classList.add("translate-x-0");
|
||||
this.sidebarEl.classList.remove("translate-x-90");
|
||||
}
|
||||
|
||||
close() {
|
||||
this.sidebarEl.classList.add("translate-x-90");
|
||||
this.sidebarEl.classList.remove("translate-x-0");
|
||||
}
|
||||
}
|
||||
|
||||
class darkMode {
|
||||
constructor() {
|
||||
this.htmlEl = document.querySelector("html");
|
||||
this.darkToggleEl = document.querySelector("[data-dark-toggle]");
|
||||
this.darkToggleLabel = document.querySelector("[data-dark-toggle-label]");
|
||||
this.csrf = document.querySelector("input#csrf_token");
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.darkToggleEl.addEventListener("change", (e) => {
|
||||
this.toggle();
|
||||
this.saveMode();
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
document.querySelector("html").classList.toggle("dark");
|
||||
this.darkToggleLabel.textContent = this.darkToggleEl.checked
|
||||
? "dark mode"
|
||||
: "light mode";
|
||||
}
|
||||
|
||||
async saveMode() {
|
||||
const isDark = this.darkToggleEl.checked ? "true" : "false";
|
||||
const data = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": this.csrf.value,
|
||||
},
|
||||
body: JSON.stringify({ darkmode: isDark }),
|
||||
};
|
||||
const send = await fetch(
|
||||
`${location.href.split("/").slice(0, -1).join("/")}/darkmode`,
|
||||
data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlashMsg {
|
||||
constructor() {
|
||||
this.openBtn = document.querySelector("[data-flash-sidebar-open]");
|
||||
this.flashCount = document.querySelector("[data-flash-count]");
|
||||
this.isMsgCheck = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
//animate message button if message + never opened
|
||||
window.addEventListener("load", (e) => {
|
||||
if (Number(this.flashCount.textContent) > 0) this.animeBtn();
|
||||
});
|
||||
//stop animate if clicked once
|
||||
this.openBtn.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute("data-flash-sidebar-open")
|
||||
) {
|
||||
this.isMsgCheck = true;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
//remove flash message and change count
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute("data-close-flash-message")
|
||||
) {
|
||||
//remove logic
|
||||
const closeBtn = e.target.closest("button");
|
||||
const flashEl = closeBtn.closest("[data-flash-message]");
|
||||
flashEl.remove();
|
||||
//update count
|
||||
this.flashCount.textContent = document.querySelectorAll(
|
||||
"[data-flash-message]"
|
||||
).length;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
animeBtn() {
|
||||
this.openBtn.classList.add("rotate-12");
|
||||
|
||||
setTimeout(() => {
|
||||
this.openBtn.classList.remove("rotate-12");
|
||||
this.openBtn.classList.add("-rotate-12");
|
||||
}, 150);
|
||||
|
||||
setTimeout(() => {
|
||||
this.openBtn.classList.remove("-rotate-12");
|
||||
}, 300);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!this.isMsgCheck) {
|
||||
this.animeBtn();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
class Loader {
|
||||
constructor() {
|
||||
this.menuContainer = document.querySelector("[data-menu-container]");
|
||||
this.logoContainer = document.querySelector("[data-loader]");
|
||||
this.logoEl = document.querySelector("[data-loader-img]");
|
||||
this.isLoading = true;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loading();
|
||||
window.addEventListener("load", (e) => {
|
||||
setTimeout(() => {
|
||||
this.logoContainer.classList.add("opacity-0");
|
||||
}, 350);
|
||||
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
this.logoContainer.classList.add("hidden");
|
||||
}, 650);
|
||||
|
||||
setTimeout(() => {
|
||||
this.logoContainer.remove();
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
|
||||
loading() {
|
||||
if ((this.isLoading = true)) {
|
||||
setTimeout(() => {
|
||||
this.logoEl.classList.toggle("scale-105");
|
||||
this.loading();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setLoader = new Loader();
|
||||
const setMenu = new Menu();
|
||||
const setNewsSidebar = new Sidebar(
|
||||
"[data-sidebar-info]",
|
||||
"[data-sidebar-info-open]",
|
||||
"[data-sidebar-info-close]"
|
||||
);
|
||||
|
||||
const setCheckbox = new Checkbox();
|
||||
const setSelect = new Select();
|
||||
const setPassword = new Password();
|
||||
const setDisabledPop = new DisabledPop();
|
||||
|
||||
const setFlashSidebar = new Sidebar(
|
||||
"[data-flash-sidebar]",
|
||||
"[data-flash-sidebar-open]",
|
||||
"[data-flash-sidebar-close]"
|
||||
);
|
||||
const setNews = new News();
|
||||
const setDarkM = new darkMode();
|
||||
const setFlash = new FlashMsg();
|
||||
import { Checkbox, Select, Password, DisabledPop } from "./utils/form.js";
|
||||
|
||||
class Menu {
|
||||
constructor() {
|
||||
this.sidebarEl = document.querySelector("[data-sidebar-menu]");
|
||||
this.toggleBtn = document.querySelector("[data-sidebar-menu-toggle]");
|
||||
this.closeBtn = document.querySelector("[data-sidebar-menu-close]");
|
||||
|
||||
this.toggleBtn.addEventListener("click", this.toggle.bind(this));
|
||||
this.closeBtn.addEventListener("click", this.close.bind(this));
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("aside").hasAttribute("data-sidebar-menu") &&
|
||||
e.target.closest("button").getAttribute("role") === "tab"
|
||||
) {
|
||||
this.close();
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.sidebarEl.classList.toggle("-translate-x-full");
|
||||
}
|
||||
|
||||
close() {
|
||||
this.sidebarEl.classList.add("-translate-x-full");
|
||||
}
|
||||
}
|
||||
|
||||
class News {
|
||||
constructor() {
|
||||
this.BASE_URL = "https://www.bunkerweb.io/";
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("load", async () => {
|
||||
try {
|
||||
const res = await fetch("https://www.bunkerweb.io/api/posts/0/2", {
|
||||
headers: {
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
return await this.render(res);
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
render(lastNews) {
|
||||
const newsContainer = document.querySelector("[data-news-container]");
|
||||
//remove default message
|
||||
newsContainer.textContent = "";
|
||||
//render last news
|
||||
lastNews.forEach((news) => {
|
||||
//get info
|
||||
const slug = news.slug;
|
||||
const img = news.photo.url;
|
||||
const excerpt = news.excerpt;
|
||||
const tags = news.tags;
|
||||
const date = news.date;
|
||||
const lastUpdate = news.lastUpdate;
|
||||
//create html card from infos
|
||||
const cardHTML = this.template(
|
||||
slug,
|
||||
img,
|
||||
excerpt,
|
||||
tags,
|
||||
date,
|
||||
lastUpdate
|
||||
);
|
||||
let cleanHTML = DOMPurify.sanitize(cardHTML);
|
||||
//add to DOM
|
||||
document
|
||||
.querySelector("[data-news-container]")
|
||||
.insertAdjacentHTML("afterbegin", cleanHTML);
|
||||
});
|
||||
}
|
||||
|
||||
template(slug, img, excerpt, tags, date, lastUpdate) {
|
||||
//loop on tags to get list
|
||||
let tagList = "";
|
||||
tags.forEach((tag) => {
|
||||
tagList += ` <a
|
||||
href="${this.BASE_URL}/blog/tag/${tag.slug}"
|
||||
class="my-0 mr-1 rounded bg-secondary hover:brightness-90 hover:-translate-y-0.4 text-white py-1 px-2 text-sm"
|
||||
>
|
||||
${tag.name}
|
||||
</a>`;
|
||||
});
|
||||
//create card
|
||||
const card = `
|
||||
<div
|
||||
class="min-h-[400px] w-full col-span-12 transition hover:-translate-y-2 bg-gray-100 dark:bg-slate-900 rounded px-6 py-4 m-2 flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<img role="link"
|
||||
onclick="window.location.href='${this.BASE_URL}/blog/post/${slug}'"
|
||||
class="cursor-pointer rounded w-full h-40 m-0 object-cover"
|
||||
src="${img}"
|
||||
alt="image"
|
||||
/>
|
||||
<h3 role="link"
|
||||
onclick="window.location.href='${this.BASE_URL}/blog/post/${slug}'"
|
||||
class="cursor-pointer mt-3 mb-1 text-3xl dark:text-white tracking-wide">{{ post['title'] }}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div role="link"
|
||||
onclick="window.location.href='${this.BASE_URL}/blog/post/${slug}'"
|
||||
class="cursor-pointer min-h-[130px] mb-3 text-lg dark:text-gray-300 text-gray-600 pt-3">
|
||||
${excerpt}
|
||||
</div>
|
||||
<div class="min-h-[75px] mt-2 flex flex-wrap justify-start items-end align-bottom">
|
||||
${tagList}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col justify-start items-start">
|
||||
<span class="text-xs dark:text-gray-300 text-gray-600"
|
||||
>Posted on : ${date}</span
|
||||
>
|
||||
{% if post["updatedAt"] %}
|
||||
<span class="text-xs dark:text-gray-300 text-gray-600"
|
||||
>Last update : ${lastUpdate}</span
|
||||
>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
</div> `;
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
class Sidebar {
|
||||
constructor(elAtt, btnOpenAtt, btnCloseAtt) {
|
||||
this.sidebarEl = document.querySelector(elAtt);
|
||||
this.openBtn = document.querySelector(btnOpenAtt);
|
||||
this.closeBtn = document.querySelector(btnCloseAtt);
|
||||
this.openBtn.addEventListener("click", this.open.bind(this));
|
||||
this.closeBtn.addEventListener("click", this.close.bind(this));
|
||||
}
|
||||
|
||||
open() {
|
||||
this.sidebarEl.classList.add("translate-x-0");
|
||||
this.sidebarEl.classList.remove("translate-x-90");
|
||||
}
|
||||
|
||||
close() {
|
||||
this.sidebarEl.classList.add("translate-x-90");
|
||||
this.sidebarEl.classList.remove("translate-x-0");
|
||||
}
|
||||
}
|
||||
|
||||
class darkMode {
|
||||
constructor() {
|
||||
this.htmlEl = document.querySelector("html");
|
||||
this.darkToggleEl = document.querySelector("[data-dark-toggle]");
|
||||
this.darkToggleLabel = document.querySelector("[data-dark-toggle-label]");
|
||||
this.csrf = document.querySelector("input#csrf_token");
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.darkToggleEl.addEventListener("change", (e) => {
|
||||
this.toggle();
|
||||
this.saveMode();
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
document.querySelector("html").classList.toggle("dark");
|
||||
this.darkToggleLabel.textContent = this.darkToggleEl.checked
|
||||
? "dark mode"
|
||||
: "light mode";
|
||||
}
|
||||
|
||||
async saveMode() {
|
||||
const isDark = this.darkToggleEl.checked ? "true" : "false";
|
||||
const data = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": this.csrf.value,
|
||||
},
|
||||
body: JSON.stringify({ darkmode: isDark }),
|
||||
};
|
||||
const send = await fetch(
|
||||
`${location.href.split("/").slice(0, -1).join("/")}/darkmode`,
|
||||
data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlashMsg {
|
||||
constructor() {
|
||||
this.openBtn = document.querySelector("[data-flash-sidebar-open]");
|
||||
this.flashCount = document.querySelector("[data-flash-count]");
|
||||
this.isMsgCheck = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
//animate message button if message + never opened
|
||||
window.addEventListener("load", (e) => {
|
||||
if (Number(this.flashCount.textContent) > 0) this.animeBtn();
|
||||
});
|
||||
//stop animate if clicked once
|
||||
this.openBtn.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute("data-flash-sidebar-open")
|
||||
) {
|
||||
this.isMsgCheck = true;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
//remove flash message and change count
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute("data-close-flash-message")
|
||||
) {
|
||||
//remove logic
|
||||
const closeBtn = e.target.closest("button");
|
||||
const flashEl = closeBtn.closest("[data-flash-message]");
|
||||
flashEl.remove();
|
||||
//update count
|
||||
this.flashCount.textContent = document.querySelectorAll(
|
||||
"[data-flash-message]"
|
||||
).length;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
animeBtn() {
|
||||
this.openBtn.classList.add("rotate-12");
|
||||
|
||||
setTimeout(() => {
|
||||
this.openBtn.classList.remove("rotate-12");
|
||||
this.openBtn.classList.add("-rotate-12");
|
||||
}, 150);
|
||||
|
||||
setTimeout(() => {
|
||||
this.openBtn.classList.remove("-rotate-12");
|
||||
}, 300);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!this.isMsgCheck) {
|
||||
this.animeBtn();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
class Loader {
|
||||
constructor() {
|
||||
this.menuContainer = document.querySelector("[data-menu-container]");
|
||||
this.logoContainer = document.querySelector("[data-loader]");
|
||||
this.logoEl = document.querySelector("[data-loader-img]");
|
||||
this.isLoading = true;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loading();
|
||||
window.addEventListener("load", (e) => {
|
||||
setTimeout(() => {
|
||||
this.logoContainer.classList.add("opacity-0");
|
||||
}, 350);
|
||||
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
this.logoContainer.classList.add("hidden");
|
||||
}, 650);
|
||||
|
||||
setTimeout(() => {
|
||||
this.logoContainer.remove();
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
|
||||
loading() {
|
||||
if ((this.isLoading = true)) {
|
||||
setTimeout(() => {
|
||||
this.logoEl.classList.toggle("scale-105");
|
||||
this.loading();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setLoader = new Loader();
|
||||
const setMenu = new Menu();
|
||||
const setNewsSidebar = new Sidebar(
|
||||
"[data-sidebar-info]",
|
||||
"[data-sidebar-info-open]",
|
||||
"[data-sidebar-info-close]"
|
||||
);
|
||||
|
||||
const setCheckbox = new Checkbox();
|
||||
const setSelect = new Select();
|
||||
const setPassword = new Password();
|
||||
const setDisabledPop = new DisabledPop();
|
||||
|
||||
const setFlashSidebar = new Sidebar(
|
||||
"[data-flash-sidebar]",
|
||||
"[data-flash-sidebar-open]",
|
||||
"[data-flash-sidebar-close]"
|
||||
);
|
||||
const setNews = new News();
|
||||
const setDarkM = new darkMode();
|
||||
const setFlash = new FlashMsg();
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
import {
|
||||
Popover,
|
||||
Tabs,
|
||||
FormatValue,
|
||||
FilterSettings,
|
||||
} from "./utils/settings.js";
|
||||
|
||||
class Multiple {
|
||||
constructor(prefix) {
|
||||
this.prefix = prefix;
|
||||
this.init();
|
||||
}
|
||||
//hide multiples handler if no multiple setting on plugin
|
||||
init() {
|
||||
//hide multiple btn if no multiple exist on a plugin
|
||||
const multiples = document.querySelectorAll(
|
||||
`[data-${this.prefix}-settings-multiple]`
|
||||
);
|
||||
multiples.forEach((container) => {
|
||||
if (container.querySelectorAll(`[data-setting-container]`).length <= 0)
|
||||
container.parentElement
|
||||
.querySelector("[data-multiple-handler]")
|
||||
.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const setPopover = new Popover("main", "global-config");
|
||||
const setTabs = new Tabs("[global-config-tabs]", "global-config");
|
||||
const format = new FormatValue();
|
||||
const setMultiple = new Multiple("global-config");
|
||||
const setFilterGlobal = new FilterSettings(
|
||||
"settings-filter",
|
||||
"[data-service-content='settings']"
|
||||
);
|
||||
import {
|
||||
Popover,
|
||||
Tabs,
|
||||
FormatValue,
|
||||
FilterSettings,
|
||||
} from "./utils/settings.js";
|
||||
|
||||
class Multiple {
|
||||
constructor(prefix) {
|
||||
this.prefix = prefix;
|
||||
this.init();
|
||||
}
|
||||
//hide multiples handler if no multiple setting on plugin
|
||||
init() {
|
||||
//hide multiple btn if no multiple exist on a plugin
|
||||
const multiples = document.querySelectorAll(
|
||||
`[data-${this.prefix}-settings-multiple]`,
|
||||
);
|
||||
multiples.forEach((container) => {
|
||||
if (container.querySelectorAll(`[data-setting-container]`).length <= 0)
|
||||
container.parentElement
|
||||
.querySelector("[data-multiple-handler]")
|
||||
.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const setPopover = new Popover("main", "global-config");
|
||||
const setTabs = new Tabs("[global-config-tabs]", "global-config");
|
||||
const format = new FormatValue();
|
||||
const setMultiple = new Multiple("global-config");
|
||||
const setFilterGlobal = new FilterSettings(
|
||||
"settings-filter",
|
||||
"[data-service-content='settings']",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class Dropdown {
|
|||
const btn = e.target.closest("button");
|
||||
const btnValue = btn.getAttribute("value");
|
||||
const btnSetting = btn.getAttribute(
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`,
|
||||
);
|
||||
//stop if same value to avoid new fetching
|
||||
const isSameVal = this.isSameValue(btnSetting, btnValue);
|
||||
|
|
@ -61,7 +61,7 @@ class Dropdown {
|
|||
|
||||
closeAllDrop() {
|
||||
const drops = document.querySelectorAll(
|
||||
`[data-${this.prefix}-setting-select-dropdown]`
|
||||
`[data-${this.prefix}-setting-select-dropdown]`,
|
||||
);
|
||||
drops.forEach((drop) => {
|
||||
drop.classList.add("hidden");
|
||||
|
|
@ -69,8 +69,8 @@ class Dropdown {
|
|||
document
|
||||
.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${drop.getAttribute(
|
||||
`data-${this.prefix}-setting-select-dropdown`
|
||||
)}"]`
|
||||
`data-${this.prefix}-setting-select-dropdown`,
|
||||
)}"]`,
|
||||
)
|
||||
.classList.remove("rotate-180");
|
||||
});
|
||||
|
|
@ -78,7 +78,7 @@ class Dropdown {
|
|||
|
||||
isSameValue(btnSetting, value) {
|
||||
const selectCustom = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`,
|
||||
);
|
||||
const currVal = selectCustom.textContent;
|
||||
return currVal === value ? true : false;
|
||||
|
|
@ -86,30 +86,30 @@ class Dropdown {
|
|||
|
||||
setSelectNewValue(btnSetting, value) {
|
||||
const selectCustom = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select="${btnSetting}"]`,
|
||||
);
|
||||
selectCustom.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text]`
|
||||
`[data-${this.prefix}-setting-select-text]`,
|
||||
).textContent = value;
|
||||
}
|
||||
|
||||
hideDropdown(btnSetting) {
|
||||
//hide dropdown
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`,
|
||||
);
|
||||
dropdownEl.classList.add("hidden");
|
||||
dropdownEl.classList.remove("flex");
|
||||
//svg effect
|
||||
const dropdownChevron = document.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${btnSetting}"]`
|
||||
`svg[data-${this.prefix}-setting-select="${btnSetting}"]`,
|
||||
);
|
||||
dropdownChevron.classList.remove("rotate-180");
|
||||
}
|
||||
|
||||
changeDropBtnStyle(btnSetting, selectedBtn) {
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`,
|
||||
);
|
||||
//reset dropdown btns
|
||||
const btnEls = dropdownEl.querySelectorAll("button");
|
||||
|
|
@ -119,7 +119,7 @@ class Dropdown {
|
|||
"bg-primary",
|
||||
"dark:bg-primary",
|
||||
"text-gray-300",
|
||||
"text-gray-300"
|
||||
"text-gray-300",
|
||||
);
|
||||
btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
|
||||
});
|
||||
|
|
@ -127,7 +127,7 @@ class Dropdown {
|
|||
selectedBtn.classList.remove(
|
||||
"bg-white",
|
||||
"dark:bg-slate-700",
|
||||
"text-gray-700"
|
||||
"text-gray-700",
|
||||
);
|
||||
selectedBtn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
|
||||
}
|
||||
|
|
@ -138,10 +138,10 @@ class Dropdown {
|
|||
.getAttribute(`data-${this.prefix}-setting-select`);
|
||||
//toggle dropdown
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${attribut}"]`
|
||||
`[data-${this.prefix}-setting-select-dropdown="${attribut}"]`,
|
||||
);
|
||||
const dropdownChevron = document.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${attribut}"]`
|
||||
`svg[data-${this.prefix}-setting-select="${attribut}"]`,
|
||||
);
|
||||
dropdownEl.classList.toggle("hidden");
|
||||
dropdownEl.classList.toggle("flex");
|
||||
|
|
@ -197,7 +197,7 @@ class Filter {
|
|||
setTimeout(() => {
|
||||
const value = document
|
||||
.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text="success"]`
|
||||
`[data-${this.prefix}-setting-select-text="success"]`,
|
||||
)
|
||||
.textContent.trim();
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ class Filter {
|
|||
setTimeout(() => {
|
||||
const value = document
|
||||
.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text="reload"]`
|
||||
`[data-${this.prefix}-setting-select-text="reload"]`,
|
||||
)
|
||||
.textContent.trim();
|
||||
|
||||
|
|
@ -329,7 +329,7 @@ class Download {
|
|||
|
||||
async sendFileToDL(jobName, fileName) {
|
||||
window.open(
|
||||
`${location.href}/download?job_name=${jobName}&file_name=${fileName}`
|
||||
`${location.href}/download?job_name=${jobName}&file_name=${fileName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class Dropdown {
|
|||
const btn = e.target.closest("button");
|
||||
const btnValue = btn.getAttribute("value");
|
||||
const btnSetting = btn.getAttribute(
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`,
|
||||
);
|
||||
//stop if same value to avoid new fetching
|
||||
const isSameVal = this.isSameValue(btnSetting, btnValue);
|
||||
|
|
@ -58,7 +58,7 @@ class Dropdown {
|
|||
|
||||
closeAllDrop() {
|
||||
const drops = document.querySelectorAll(
|
||||
`[data-${this.prefix}-setting-select-dropdown]`
|
||||
`[data-${this.prefix}-setting-select-dropdown]`,
|
||||
);
|
||||
drops.forEach((drop) => {
|
||||
drop.classList.add("hidden");
|
||||
|
|
@ -66,8 +66,8 @@ class Dropdown {
|
|||
document
|
||||
.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${drop.getAttribute(
|
||||
`data-${this.prefix}-setting-select-dropdown`
|
||||
)}"]`
|
||||
`data-${this.prefix}-setting-select-dropdown`,
|
||||
)}"]`,
|
||||
)
|
||||
.classList.remove("rotate-180");
|
||||
});
|
||||
|
|
@ -75,7 +75,7 @@ class Dropdown {
|
|||
|
||||
isSameValue(btnSetting, value) {
|
||||
const selectCustom = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`,
|
||||
);
|
||||
const currVal = selectCustom.textContent;
|
||||
return currVal === value ? true : false;
|
||||
|
|
@ -83,30 +83,30 @@ class Dropdown {
|
|||
|
||||
setSelectNewValue(btnSetting, value) {
|
||||
const selectCustom = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select="${btnSetting}"]`,
|
||||
);
|
||||
selectCustom.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text]`
|
||||
`[data-${this.prefix}-setting-select-text]`,
|
||||
).textContent = value;
|
||||
}
|
||||
|
||||
hideDropdown(btnSetting) {
|
||||
//hide dropdown
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`,
|
||||
);
|
||||
dropdownEl.classList.add("hidden");
|
||||
dropdownEl.classList.remove("flex");
|
||||
//svg effect
|
||||
const dropdownChevron = document.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${btnSetting}"]`
|
||||
`svg[data-${this.prefix}-setting-select="${btnSetting}"]`,
|
||||
);
|
||||
dropdownChevron.classList.remove("rotate-180");
|
||||
}
|
||||
|
||||
changeDropBtnStyle(btnSetting, selectedBtn) {
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`,
|
||||
);
|
||||
//reset dropdown btns
|
||||
const btnEls = dropdownEl.querySelectorAll("button");
|
||||
|
|
@ -116,7 +116,7 @@ class Dropdown {
|
|||
"bg-primary",
|
||||
"dark:bg-primary",
|
||||
"text-gray-300",
|
||||
"text-gray-300"
|
||||
"text-gray-300",
|
||||
);
|
||||
btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
|
||||
});
|
||||
|
|
@ -124,7 +124,7 @@ class Dropdown {
|
|||
selectedBtn.classList.remove(
|
||||
"bg-white",
|
||||
"dark:bg-slate-700",
|
||||
"text-gray-700"
|
||||
"text-gray-700",
|
||||
);
|
||||
selectedBtn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
|
||||
}
|
||||
|
|
@ -135,10 +135,10 @@ class Dropdown {
|
|||
.getAttribute(`data-${this.prefix}-setting-select`);
|
||||
//toggle dropdown
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${attribut}"]`
|
||||
`[data-${this.prefix}-setting-select-dropdown="${attribut}"]`,
|
||||
);
|
||||
const dropdownChevron = document.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${attribut}"]`
|
||||
`svg[data-${this.prefix}-setting-select="${attribut}"]`,
|
||||
);
|
||||
dropdownEl.classList.toggle("hidden");
|
||||
dropdownEl.classList.toggle("flex");
|
||||
|
|
@ -173,7 +173,7 @@ class FetchLogs {
|
|||
constructor(prefix = "logs") {
|
||||
this.prefix = prefix;
|
||||
this.instance = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text="instances"]`
|
||||
`[data-${this.prefix}-setting-select-text="instances"]`,
|
||||
);
|
||||
this.instanceName = "";
|
||||
this.updateInp = document.querySelector("input#update-date");
|
||||
|
|
@ -188,7 +188,7 @@ class FetchLogs {
|
|||
this.lastUpdate = Date.now() - 86400000;
|
||||
this.container = document.querySelector(`[data-${this.prefix}-settings]`);
|
||||
this.logListContainer = document.querySelector(
|
||||
`[data-${this.prefix}-list]`
|
||||
`[data-${this.prefix}-list]`,
|
||||
);
|
||||
this.submitDate = document.querySelector("button[data-submit-date]");
|
||||
this.submitLive = document.querySelector("button[data-submit-live]");
|
||||
|
|
@ -218,7 +218,7 @@ class FetchLogs {
|
|||
"data-submit-live",
|
||||
this.submitLive.getAttribute("data-submit-live") === "yes"
|
||||
? "no"
|
||||
: "yes"
|
||||
: "yes",
|
||||
);
|
||||
|
||||
if (this.submitLive.getAttribute("data-submit-live") === "yes") {
|
||||
|
|
@ -314,7 +314,7 @@ class FetchLogs {
|
|||
.querySelector(`[data-${this.prefix}-list]`)
|
||||
.scrollTo(
|
||||
0,
|
||||
document.querySelector(`[data-${this.prefix}-list]`).scrollHeight
|
||||
document.querySelector(`[data-${this.prefix}-list]`).scrollHeight,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +323,7 @@ class FetchLogs {
|
|||
//case from date defined only
|
||||
if (this.toDate) {
|
||||
res = await fetch(
|
||||
`${location.href}/${this.instanceName}?from_date=${this.fromDate}&to_date=${this.toDate}`
|
||||
`${location.href}/${this.instanceName}?from_date=${this.fromDate}&to_date=${this.toDate}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
return await this.showLogsDate(data);
|
||||
|
|
@ -331,7 +331,7 @@ class FetchLogs {
|
|||
//case from date and to date defined
|
||||
if (!this.toDate) {
|
||||
res = await fetch(
|
||||
`${location.href}/${this.instanceName}?from_date=${this.fromDate}`
|
||||
`${location.href}/${this.instanceName}?from_date=${this.fromDate}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
return await this.showLogsDate(data);
|
||||
|
|
@ -341,7 +341,7 @@ class FetchLogs {
|
|||
async getLogsSinceLastUpdate() {
|
||||
const response = await fetch(
|
||||
`${location.href}/${this.instanceName}` +
|
||||
(this.lastUpdate ? `?last_update=${this.lastUpdate}` : "")
|
||||
(this.lastUpdate ? `?last_update=${this.lastUpdate}` : ""),
|
||||
);
|
||||
const data = await response.json();
|
||||
return await this.showLogsLive(data);
|
||||
|
|
@ -437,7 +437,7 @@ class Filter {
|
|||
const btn = e.target.closest("button");
|
||||
const btnValue = btn.getAttribute("value");
|
||||
const btnSetting = btn.getAttribute(
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`,
|
||||
);
|
||||
|
||||
this.lastType = btnValue;
|
||||
|
|
|
|||
|
|
@ -286,7 +286,9 @@ class Upload {
|
|||
//close fail/success log
|
||||
this.container.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (e.target.closest("button").hasAttribute("data-upload-message-delete")) {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute("data-upload-message-delete")
|
||||
) {
|
||||
const message = e.target.closest("div[data-upload-message]");
|
||||
message.remove();
|
||||
}
|
||||
|
|
@ -406,7 +408,8 @@ class Upload {
|
|||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return str;
|
||||
let cleanHTML = DOMPurify.sanitize(str);
|
||||
return cleanHTML;
|
||||
}
|
||||
|
||||
fileFail(name, fileSize) {
|
||||
|
|
@ -433,7 +436,8 @@ class Upload {
|
|||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return str;
|
||||
let cleanHTML = DOMPurify.sanitize(str);
|
||||
return cleanHTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,8 +449,12 @@ class Modal {
|
|||
this.modalNameInp = this.modal.querySelector("input#name");
|
||||
this.modalExtInp = this.modal.querySelector("input#external");
|
||||
|
||||
this.modalTitle = this.modal.querySelector(`[data-${this.prefix}-modal-title]`);
|
||||
this.modalTxt = this.modal.querySelector(`[data-${this.prefix}-modal-text]`);
|
||||
this.modalTitle = this.modal.querySelector(
|
||||
`[data-${this.prefix}-modal-title]`
|
||||
);
|
||||
this.modalTxt = this.modal.querySelector(
|
||||
`[data-${this.prefix}-modal-text]`
|
||||
);
|
||||
this.init();
|
||||
}
|
||||
|
||||
|
|
@ -455,8 +463,9 @@ class Modal {
|
|||
//DELETE HANDLER
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").getAttribute(`data-${this.prefix}-action`) ===
|
||||
"delete"
|
||||
e.target
|
||||
.closest("button")
|
||||
.getAttribute(`data-${this.prefix}-action`) === "delete"
|
||||
) {
|
||||
const btnEl = e.target.closest("button");
|
||||
this.setModal(btnEl);
|
||||
|
|
@ -469,7 +478,9 @@ class Modal {
|
|||
//CLOSE MODAL HANDLER
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute(`data-${this.prefix}-modal-close`)
|
||||
e.target
|
||||
.closest("button")
|
||||
.hasAttribute(`data-${this.prefix}-modal-close`)
|
||||
) {
|
||||
this.hideModal();
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
4
src/ui/static/js/tsparticles.bundle.min.js
vendored
4
src/ui/static/js/tsparticles.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -1,262 +1,263 @@
|
|||
class Checkbox {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
//prevent default checkbox behavior
|
||||
try {
|
||||
//case a related checkbox element is clicked and checkbox is enabled
|
||||
if (
|
||||
e.target.closest("div").hasAttribute("data-checkbox-handler") &&
|
||||
!e.target
|
||||
.closest("div")
|
||||
.querySelector('input[type="checkbox"]')
|
||||
.hasAttribute("disabled")
|
||||
) {
|
||||
//get related checkbox
|
||||
const checkboxEl = e.target
|
||||
.closest("div")
|
||||
.querySelector('input[type="checkbox"]');
|
||||
|
||||
const prevValue = checkboxEl.getAttribute("value");
|
||||
|
||||
//set attribute value for new state
|
||||
prevValue === "no"
|
||||
? checkboxEl.setAttribute("value", "yes")
|
||||
: checkboxEl.setAttribute("value", "no");
|
||||
|
||||
//set custom input hidden value
|
||||
const newValue = checkboxEl.getAttribute("value");
|
||||
newValue === "yes"
|
||||
? checkboxEl.setAttribute("aria-checked", "true")
|
||||
: checkboxEl.setAttribute("aria-checked", "false");
|
||||
|
||||
//force checked for submit
|
||||
checkboxEl.checked = true;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Select {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
//CASE NO BTN SELECT CLICKED
|
||||
try {
|
||||
if (!e.target.closest("button")) {
|
||||
const selectEls = document.querySelectorAll(
|
||||
"div[data-setting-select-dropdown]"
|
||||
);
|
||||
selectEls.forEach((select) => {
|
||||
select.classList.add("hidden");
|
||||
select.classList.remove("flex");
|
||||
});
|
||||
const btnEls = document.querySelectorAll(
|
||||
"button[data-setting-select]"
|
||||
);
|
||||
btnEls.forEach((btn) => {
|
||||
const dropdownChevron = btn.querySelector(
|
||||
`svg[data-setting-select]`
|
||||
);
|
||||
dropdownChevron.classList.remove("rotate-180");
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
//SELECT BTN LOGIC
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute(`data-setting-select`) &&
|
||||
!e.target.closest("button").hasAttribute(`disabled`)
|
||||
) {
|
||||
const btnEl = e.target.closest("button");
|
||||
this.toggleSelectBtn(btnEl);
|
||||
}
|
||||
} catch (err) {}
|
||||
//SELECT DROPDOWN BTN LOGIC
|
||||
try {
|
||||
if (
|
||||
e.target
|
||||
.closest("button")
|
||||
.hasAttribute(`data-setting-select-dropdown-btn`)
|
||||
) {
|
||||
const btn = e.target.closest(
|
||||
`button[data-setting-select-dropdown-btn]`
|
||||
);
|
||||
const btnValue = btn.getAttribute("value");
|
||||
|
||||
//add new value to custom
|
||||
const selectCustom = btn
|
||||
.closest("div[data-select-container]")
|
||||
.querySelector(`button[data-setting-select]`);
|
||||
|
||||
selectCustom.querySelector(`[data-setting-select-text]`).textContent =
|
||||
btnValue;
|
||||
//add selected to new value
|
||||
|
||||
//change style
|
||||
const dropdownEl = btn.closest(`div[data-setting-select-dropdown]`);
|
||||
dropdownEl.classList.add("hidden");
|
||||
dropdownEl.classList.remove("flex");
|
||||
|
||||
//reset dropdown btns
|
||||
const btnEls = dropdownEl.querySelectorAll("button");
|
||||
|
||||
btnEls.forEach((btn) => {
|
||||
btn.classList.remove("active");
|
||||
});
|
||||
//highlight clicked btn
|
||||
btn.classList.add("active");
|
||||
|
||||
//close dropdown
|
||||
const dropdownChevron = selectCustom.querySelector(
|
||||
`svg[data-setting-select]`
|
||||
);
|
||||
dropdownChevron.classList.remove("rotate-180");
|
||||
|
||||
//update real select element
|
||||
const realSel = btn
|
||||
.closest("div[data-setting-container]")
|
||||
.querySelector("select");
|
||||
this.updateSelected(realSel, btnValue);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
updateSelected(selectEl, selectedValue) {
|
||||
const options = selectEl.querySelectorAll("option");
|
||||
//remove selected to all
|
||||
options.forEach((option) => {
|
||||
option.removeAttribute("selected");
|
||||
option.selected = false;
|
||||
});
|
||||
//select new one
|
||||
const newOption = selectEl.querySelector(
|
||||
`option[value="${selectedValue}"]`
|
||||
);
|
||||
newOption.selected = true;
|
||||
newOption.setAttribute("selected", "");
|
||||
}
|
||||
|
||||
toggleSelectBtn(btn) {
|
||||
//toggle dropdown
|
||||
const dropdownEl = btn
|
||||
.closest("div")
|
||||
.querySelector(`[data-setting-select-dropdown]`);
|
||||
const dropdownChevron = btn.querySelector(`svg[data-setting-select]`);
|
||||
dropdownEl.classList.toggle("hidden");
|
||||
dropdownEl.classList.toggle("flex");
|
||||
dropdownChevron.classList.toggle("rotate-180");
|
||||
}
|
||||
}
|
||||
|
||||
class Password {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (e.target.closest("button").hasAttribute("data-setting-password")) {
|
||||
const btn = e.target.closest("button");
|
||||
const action = btn.getAttribute("data-setting-password");
|
||||
const inp = btn
|
||||
.closest("[data-setting-container]")
|
||||
.querySelector("input");
|
||||
this.setValDisplay(action, inp);
|
||||
this.hiddenBtns(btn);
|
||||
this.showOppositeBtn(btn, action);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
showOppositeBtn(btnEl, action) {
|
||||
const btnEls = this.getBtns(btnEl);
|
||||
const opposite = action === "visible" ? "invisible" : "visible";
|
||||
|
||||
btnEls.forEach((btn) => {
|
||||
const action = btn.getAttribute("data-setting-password");
|
||||
|
||||
if (action === opposite) {
|
||||
btn.classList.add("flex");
|
||||
btn.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setValDisplay(action, inp) {
|
||||
if (action === "visible") inp.setAttribute("type", "text");
|
||||
if (action === "invisible") inp.setAttribute("type", "password");
|
||||
}
|
||||
|
||||
hiddenBtns(btnEl) {
|
||||
const btnEls = this.getBtns(btnEl);
|
||||
|
||||
btnEls.forEach((btn) => {
|
||||
btn.classList.add("hidden");
|
||||
btn.classList.remove("flex");
|
||||
});
|
||||
}
|
||||
|
||||
getBtns(btnEl) {
|
||||
return btnEl
|
||||
.closest("[data-setting-container]")
|
||||
.querySelectorAll("button[data-setting-password]");
|
||||
}
|
||||
}
|
||||
|
||||
class DisabledPop {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("pointerover", (e) => {
|
||||
//for checkbox and regular inputs
|
||||
if (e.target.tagName === "INPUT") {
|
||||
const el = e.target;
|
||||
this.showPopup(el, "input");
|
||||
}
|
||||
//for select custom
|
||||
if (
|
||||
e.target.tagName === "BUTTON" &&
|
||||
e.target.hasAttribute("data-setting-select")
|
||||
) {
|
||||
const el = e.target;
|
||||
this.showPopup(el, "select");
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("pointerout", (e) => {
|
||||
try {
|
||||
const popupEl = e.target
|
||||
.closest("div")
|
||||
.querySelector("div[data-disabled-info]");
|
||||
popupEl.remove();
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
showPopup(el, type = "input") {
|
||||
if (!el.hasAttribute("disabled")) return;
|
||||
const method = el.getAttribute("data-default-method");
|
||||
const popupHTML = `
|
||||
<div data-disabled-info class="${
|
||||
type === "select" ? "translate-y-2" : ""
|
||||
} bg-blue-500 absolute right-2 rounded-lg px-2 py-1 z-20 dark:brightness-90">
|
||||
<p class="m-0 text-xs text-white dark:text-gray-100">disabled by ${method}</p>
|
||||
</div>`;
|
||||
el.insertAdjacentHTML("beforebegin", popupHTML);
|
||||
}
|
||||
}
|
||||
|
||||
export { Checkbox, Select, Password, DisabledPop };
|
||||
class Checkbox {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
//prevent default checkbox behavior
|
||||
try {
|
||||
//case a related checkbox element is clicked and checkbox is enabled
|
||||
if (
|
||||
e.target.closest("div").hasAttribute("data-checkbox-handler") &&
|
||||
!e.target
|
||||
.closest("div")
|
||||
.querySelector('input[type="checkbox"]')
|
||||
.hasAttribute("disabled")
|
||||
) {
|
||||
//get related checkbox
|
||||
const checkboxEl = e.target
|
||||
.closest("div")
|
||||
.querySelector('input[type="checkbox"]');
|
||||
|
||||
const prevValue = checkboxEl.getAttribute("value");
|
||||
|
||||
//set attribute value for new state
|
||||
prevValue === "no"
|
||||
? checkboxEl.setAttribute("value", "yes")
|
||||
: checkboxEl.setAttribute("value", "no");
|
||||
|
||||
//set custom input hidden value
|
||||
const newValue = checkboxEl.getAttribute("value");
|
||||
newValue === "yes"
|
||||
? checkboxEl.setAttribute("aria-checked", "true")
|
||||
: checkboxEl.setAttribute("aria-checked", "false");
|
||||
|
||||
//force checked for submit
|
||||
checkboxEl.checked = true;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Select {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
//CASE NO BTN SELECT CLICKED
|
||||
try {
|
||||
if (!e.target.closest("button")) {
|
||||
const selectEls = document.querySelectorAll(
|
||||
"div[data-setting-select-dropdown]"
|
||||
);
|
||||
selectEls.forEach((select) => {
|
||||
select.classList.add("hidden");
|
||||
select.classList.remove("flex");
|
||||
});
|
||||
const btnEls = document.querySelectorAll(
|
||||
"button[data-setting-select]"
|
||||
);
|
||||
btnEls.forEach((btn) => {
|
||||
const dropdownChevron = btn.querySelector(
|
||||
`svg[data-setting-select]`
|
||||
);
|
||||
dropdownChevron.classList.remove("rotate-180");
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
//SELECT BTN LOGIC
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute(`data-setting-select`) &&
|
||||
!e.target.closest("button").hasAttribute(`disabled`)
|
||||
) {
|
||||
const btnEl = e.target.closest("button");
|
||||
this.toggleSelectBtn(btnEl);
|
||||
}
|
||||
} catch (err) {}
|
||||
//SELECT DROPDOWN BTN LOGIC
|
||||
try {
|
||||
if (
|
||||
e.target
|
||||
.closest("button")
|
||||
.hasAttribute(`data-setting-select-dropdown-btn`)
|
||||
) {
|
||||
const btn = e.target.closest(
|
||||
`button[data-setting-select-dropdown-btn]`
|
||||
);
|
||||
const btnValue = btn.getAttribute("value");
|
||||
|
||||
//add new value to custom
|
||||
const selectCustom = btn
|
||||
.closest("div[data-select-container]")
|
||||
.querySelector(`button[data-setting-select]`);
|
||||
|
||||
selectCustom.querySelector(`[data-setting-select-text]`).textContent =
|
||||
btnValue;
|
||||
//add selected to new value
|
||||
|
||||
//change style
|
||||
const dropdownEl = btn.closest(`div[data-setting-select-dropdown]`);
|
||||
dropdownEl.classList.add("hidden");
|
||||
dropdownEl.classList.remove("flex");
|
||||
|
||||
//reset dropdown btns
|
||||
const btnEls = dropdownEl.querySelectorAll("button");
|
||||
|
||||
btnEls.forEach((btn) => {
|
||||
btn.classList.remove("active");
|
||||
});
|
||||
//highlight clicked btn
|
||||
btn.classList.add("active");
|
||||
|
||||
//close dropdown
|
||||
const dropdownChevron = selectCustom.querySelector(
|
||||
`svg[data-setting-select]`
|
||||
);
|
||||
dropdownChevron.classList.remove("rotate-180");
|
||||
|
||||
//update real select element
|
||||
const realSel = btn
|
||||
.closest("div[data-setting-container]")
|
||||
.querySelector("select");
|
||||
this.updateSelected(realSel, btnValue);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
updateSelected(selectEl, selectedValue) {
|
||||
const options = selectEl.querySelectorAll("option");
|
||||
//remove selected to all
|
||||
options.forEach((option) => {
|
||||
option.removeAttribute("selected");
|
||||
option.selected = false;
|
||||
});
|
||||
//select new one
|
||||
const newOption = selectEl.querySelector(
|
||||
`option[value="${selectedValue}"]`
|
||||
);
|
||||
newOption.selected = true;
|
||||
newOption.setAttribute("selected", "");
|
||||
}
|
||||
|
||||
toggleSelectBtn(btn) {
|
||||
//toggle dropdown
|
||||
const dropdownEl = btn
|
||||
.closest("div")
|
||||
.querySelector(`[data-setting-select-dropdown]`);
|
||||
const dropdownChevron = btn.querySelector(`svg[data-setting-select]`);
|
||||
dropdownEl.classList.toggle("hidden");
|
||||
dropdownEl.classList.toggle("flex");
|
||||
dropdownChevron.classList.toggle("rotate-180");
|
||||
}
|
||||
}
|
||||
|
||||
class Password {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (e.target.closest("button").hasAttribute("data-setting-password")) {
|
||||
const btn = e.target.closest("button");
|
||||
const action = btn.getAttribute("data-setting-password");
|
||||
const inp = btn
|
||||
.closest("[data-setting-container]")
|
||||
.querySelector("input");
|
||||
this.setValDisplay(action, inp);
|
||||
this.hiddenBtns(btn);
|
||||
this.showOppositeBtn(btn, action);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
showOppositeBtn(btnEl, action) {
|
||||
const btnEls = this.getBtns(btnEl);
|
||||
const opposite = action === "visible" ? "invisible" : "visible";
|
||||
|
||||
btnEls.forEach((btn) => {
|
||||
const action = btn.getAttribute("data-setting-password");
|
||||
|
||||
if (action === opposite) {
|
||||
btn.classList.add("flex");
|
||||
btn.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setValDisplay(action, inp) {
|
||||
if (action === "visible") inp.setAttribute("type", "text");
|
||||
if (action === "invisible") inp.setAttribute("type", "password");
|
||||
}
|
||||
|
||||
hiddenBtns(btnEl) {
|
||||
const btnEls = this.getBtns(btnEl);
|
||||
|
||||
btnEls.forEach((btn) => {
|
||||
btn.classList.add("hidden");
|
||||
btn.classList.remove("flex");
|
||||
});
|
||||
}
|
||||
|
||||
getBtns(btnEl) {
|
||||
return btnEl
|
||||
.closest("[data-setting-container]")
|
||||
.querySelectorAll("button[data-setting-password]");
|
||||
}
|
||||
}
|
||||
|
||||
class DisabledPop {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("pointerover", (e) => {
|
||||
//for checkbox and regular inputs
|
||||
if (e.target.tagName === "INPUT") {
|
||||
const el = e.target;
|
||||
this.showPopup(el, "input");
|
||||
}
|
||||
//for select custom
|
||||
if (
|
||||
e.target.tagName === "BUTTON" &&
|
||||
e.target.hasAttribute("data-setting-select")
|
||||
) {
|
||||
const el = e.target;
|
||||
this.showPopup(el, "select");
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("pointerout", (e) => {
|
||||
try {
|
||||
const popupEl = e.target
|
||||
.closest("div")
|
||||
.querySelector("div[data-disabled-info]");
|
||||
popupEl.remove();
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
showPopup(el, type = "input") {
|
||||
if (!el.hasAttribute("disabled")) return;
|
||||
const method = el.getAttribute("data-default-method");
|
||||
const popupHTML = `
|
||||
<div data-disabled-info class="${
|
||||
type === "select" ? "translate-y-2" : ""
|
||||
} bg-blue-500 absolute right-2 rounded-lg px-2 py-1 z-20 dark:brightness-90">
|
||||
<p class="m-0 text-xs text-white dark:text-gray-100">disabled by ${method}</p>
|
||||
</div>`;
|
||||
let cleanHTML = DOMPurify.sanitize(popupHTML);
|
||||
el.insertAdjacentHTML("beforebegin", cleanHTML);
|
||||
}
|
||||
}
|
||||
|
||||
export { Checkbox, Select, Password, DisabledPop };
|
||||
|
|
|
|||
3
src/ui/static/js/utils/purify/purify.min.js
vendored
Normal file
3
src/ui/static/js/utils/purify/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/ui/static/js/utils/purify/purify.min.js.map
Normal file
1
src/ui/static/js/utils/purify/purify.min.js.map
Normal file
File diff suppressed because one or more lines are too long
362
src/ui/static/js/utils/purify/src/attrs.js
Normal file
362
src/ui/static/js/utils/purify/src/attrs.js
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
import { freeze } from './utils.js';
|
||||
|
||||
export const html = freeze([
|
||||
'accept',
|
||||
'action',
|
||||
'align',
|
||||
'alt',
|
||||
'autocapitalize',
|
||||
'autocomplete',
|
||||
'autopictureinpicture',
|
||||
'autoplay',
|
||||
'background',
|
||||
'bgcolor',
|
||||
'border',
|
||||
'capture',
|
||||
'cellpadding',
|
||||
'cellspacing',
|
||||
'checked',
|
||||
'cite',
|
||||
'class',
|
||||
'clear',
|
||||
'color',
|
||||
'cols',
|
||||
'colspan',
|
||||
'controls',
|
||||
'controlslist',
|
||||
'coords',
|
||||
'crossorigin',
|
||||
'datetime',
|
||||
'decoding',
|
||||
'default',
|
||||
'dir',
|
||||
'disabled',
|
||||
'disablepictureinpicture',
|
||||
'disableremoteplayback',
|
||||
'download',
|
||||
'draggable',
|
||||
'enctype',
|
||||
'enterkeyhint',
|
||||
'face',
|
||||
'for',
|
||||
'headers',
|
||||
'height',
|
||||
'hidden',
|
||||
'high',
|
||||
'href',
|
||||
'hreflang',
|
||||
'id',
|
||||
'inputmode',
|
||||
'integrity',
|
||||
'ismap',
|
||||
'kind',
|
||||
'label',
|
||||
'lang',
|
||||
'list',
|
||||
'loading',
|
||||
'loop',
|
||||
'low',
|
||||
'max',
|
||||
'maxlength',
|
||||
'media',
|
||||
'method',
|
||||
'min',
|
||||
'minlength',
|
||||
'multiple',
|
||||
'muted',
|
||||
'name',
|
||||
'nonce',
|
||||
'noshade',
|
||||
'novalidate',
|
||||
'nowrap',
|
||||
'open',
|
||||
'optimum',
|
||||
'pattern',
|
||||
'placeholder',
|
||||
'playsinline',
|
||||
'poster',
|
||||
'preload',
|
||||
'pubdate',
|
||||
'radiogroup',
|
||||
'readonly',
|
||||
'rel',
|
||||
'required',
|
||||
'rev',
|
||||
'reversed',
|
||||
'role',
|
||||
'rows',
|
||||
'rowspan',
|
||||
'spellcheck',
|
||||
'scope',
|
||||
'selected',
|
||||
'shape',
|
||||
'size',
|
||||
'sizes',
|
||||
'span',
|
||||
'srclang',
|
||||
'start',
|
||||
'src',
|
||||
'srcset',
|
||||
'step',
|
||||
'style',
|
||||
'summary',
|
||||
'tabindex',
|
||||
'title',
|
||||
'translate',
|
||||
'type',
|
||||
'usemap',
|
||||
'valign',
|
||||
'value',
|
||||
'width',
|
||||
'xmlns',
|
||||
'slot',
|
||||
]);
|
||||
|
||||
export const svg = freeze([
|
||||
'accent-height',
|
||||
'accumulate',
|
||||
'additive',
|
||||
'alignment-baseline',
|
||||
'ascent',
|
||||
'attributename',
|
||||
'attributetype',
|
||||
'azimuth',
|
||||
'basefrequency',
|
||||
'baseline-shift',
|
||||
'begin',
|
||||
'bias',
|
||||
'by',
|
||||
'class',
|
||||
'clip',
|
||||
'clippathunits',
|
||||
'clip-path',
|
||||
'clip-rule',
|
||||
'color',
|
||||
'color-interpolation',
|
||||
'color-interpolation-filters',
|
||||
'color-profile',
|
||||
'color-rendering',
|
||||
'cx',
|
||||
'cy',
|
||||
'd',
|
||||
'dx',
|
||||
'dy',
|
||||
'diffuseconstant',
|
||||
'direction',
|
||||
'display',
|
||||
'divisor',
|
||||
'dur',
|
||||
'edgemode',
|
||||
'elevation',
|
||||
'end',
|
||||
'fill',
|
||||
'fill-opacity',
|
||||
'fill-rule',
|
||||
'filter',
|
||||
'filterunits',
|
||||
'flood-color',
|
||||
'flood-opacity',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-size-adjust',
|
||||
'font-stretch',
|
||||
'font-style',
|
||||
'font-variant',
|
||||
'font-weight',
|
||||
'fx',
|
||||
'fy',
|
||||
'g1',
|
||||
'g2',
|
||||
'glyph-name',
|
||||
'glyphref',
|
||||
'gradientunits',
|
||||
'gradienttransform',
|
||||
'height',
|
||||
'href',
|
||||
'id',
|
||||
'image-rendering',
|
||||
'in',
|
||||
'in2',
|
||||
'k',
|
||||
'k1',
|
||||
'k2',
|
||||
'k3',
|
||||
'k4',
|
||||
'kerning',
|
||||
'keypoints',
|
||||
'keysplines',
|
||||
'keytimes',
|
||||
'lang',
|
||||
'lengthadjust',
|
||||
'letter-spacing',
|
||||
'kernelmatrix',
|
||||
'kernelunitlength',
|
||||
'lighting-color',
|
||||
'local',
|
||||
'marker-end',
|
||||
'marker-mid',
|
||||
'marker-start',
|
||||
'markerheight',
|
||||
'markerunits',
|
||||
'markerwidth',
|
||||
'maskcontentunits',
|
||||
'maskunits',
|
||||
'max',
|
||||
'mask',
|
||||
'media',
|
||||
'method',
|
||||
'mode',
|
||||
'min',
|
||||
'name',
|
||||
'numoctaves',
|
||||
'offset',
|
||||
'operator',
|
||||
'opacity',
|
||||
'order',
|
||||
'orient',
|
||||
'orientation',
|
||||
'origin',
|
||||
'overflow',
|
||||
'paint-order',
|
||||
'path',
|
||||
'pathlength',
|
||||
'patterncontentunits',
|
||||
'patterntransform',
|
||||
'patternunits',
|
||||
'points',
|
||||
'preservealpha',
|
||||
'preserveaspectratio',
|
||||
'primitiveunits',
|
||||
'r',
|
||||
'rx',
|
||||
'ry',
|
||||
'radius',
|
||||
'refx',
|
||||
'refy',
|
||||
'repeatcount',
|
||||
'repeatdur',
|
||||
'restart',
|
||||
'result',
|
||||
'rotate',
|
||||
'scale',
|
||||
'seed',
|
||||
'shape-rendering',
|
||||
'specularconstant',
|
||||
'specularexponent',
|
||||
'spreadmethod',
|
||||
'startoffset',
|
||||
'stddeviation',
|
||||
'stitchtiles',
|
||||
'stop-color',
|
||||
'stop-opacity',
|
||||
'stroke-dasharray',
|
||||
'stroke-dashoffset',
|
||||
'stroke-linecap',
|
||||
'stroke-linejoin',
|
||||
'stroke-miterlimit',
|
||||
'stroke-opacity',
|
||||
'stroke',
|
||||
'stroke-width',
|
||||
'style',
|
||||
'surfacescale',
|
||||
'systemlanguage',
|
||||
'tabindex',
|
||||
'targetx',
|
||||
'targety',
|
||||
'transform',
|
||||
'transform-origin',
|
||||
'text-anchor',
|
||||
'text-decoration',
|
||||
'text-rendering',
|
||||
'textlength',
|
||||
'type',
|
||||
'u1',
|
||||
'u2',
|
||||
'unicode',
|
||||
'values',
|
||||
'viewbox',
|
||||
'visibility',
|
||||
'version',
|
||||
'vert-adv-y',
|
||||
'vert-origin-x',
|
||||
'vert-origin-y',
|
||||
'width',
|
||||
'word-spacing',
|
||||
'wrap',
|
||||
'writing-mode',
|
||||
'xchannelselector',
|
||||
'ychannelselector',
|
||||
'x',
|
||||
'x1',
|
||||
'x2',
|
||||
'xmlns',
|
||||
'y',
|
||||
'y1',
|
||||
'y2',
|
||||
'z',
|
||||
'zoomandpan',
|
||||
]);
|
||||
|
||||
export const mathMl = freeze([
|
||||
'accent',
|
||||
'accentunder',
|
||||
'align',
|
||||
'bevelled',
|
||||
'close',
|
||||
'columnsalign',
|
||||
'columnlines',
|
||||
'columnspan',
|
||||
'denomalign',
|
||||
'depth',
|
||||
'dir',
|
||||
'display',
|
||||
'displaystyle',
|
||||
'encoding',
|
||||
'fence',
|
||||
'frame',
|
||||
'height',
|
||||
'href',
|
||||
'id',
|
||||
'largeop',
|
||||
'length',
|
||||
'linethickness',
|
||||
'lspace',
|
||||
'lquote',
|
||||
'mathbackground',
|
||||
'mathcolor',
|
||||
'mathsize',
|
||||
'mathvariant',
|
||||
'maxsize',
|
||||
'minsize',
|
||||
'movablelimits',
|
||||
'notation',
|
||||
'numalign',
|
||||
'open',
|
||||
'rowalign',
|
||||
'rowlines',
|
||||
'rowspacing',
|
||||
'rowspan',
|
||||
'rspace',
|
||||
'rquote',
|
||||
'scriptlevel',
|
||||
'scriptminsize',
|
||||
'scriptsizemultiplier',
|
||||
'selection',
|
||||
'separator',
|
||||
'separators',
|
||||
'stretchy',
|
||||
'subscriptshift',
|
||||
'supscriptshift',
|
||||
'symmetric',
|
||||
'voffset',
|
||||
'width',
|
||||
'xmlns',
|
||||
]);
|
||||
|
||||
export const xml = freeze([
|
||||
'xlink:href',
|
||||
'xml:id',
|
||||
'xlink:title',
|
||||
'xml:space',
|
||||
'xmlns:xlink',
|
||||
]);
|
||||
1637
src/ui/static/js/utils/purify/src/purify.js
Normal file
1637
src/ui/static/js/utils/purify/src/purify.js
Normal file
File diff suppressed because it is too large
Load diff
16
src/ui/static/js/utils/purify/src/regexp.js
Normal file
16
src/ui/static/js/utils/purify/src/regexp.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { seal } from './utils.js';
|
||||
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
export const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
|
||||
export const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
|
||||
export const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm);
|
||||
export const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape
|
||||
export const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
|
||||
export const IS_ALLOWED_URI = seal(
|
||||
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
|
||||
);
|
||||
export const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
|
||||
export const ATTR_WHITESPACE = seal(
|
||||
/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
|
||||
);
|
||||
export const DOCTYPE_NAME = seal(/^html$/i);
|
||||
280
src/ui/static/js/utils/purify/src/tags.js
Normal file
280
src/ui/static/js/utils/purify/src/tags.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
import { freeze } from './utils.js';
|
||||
|
||||
export const html = freeze([
|
||||
'a',
|
||||
'abbr',
|
||||
'acronym',
|
||||
'address',
|
||||
'area',
|
||||
'article',
|
||||
'aside',
|
||||
'audio',
|
||||
'b',
|
||||
'bdi',
|
||||
'bdo',
|
||||
'big',
|
||||
'blink',
|
||||
'blockquote',
|
||||
'body',
|
||||
'br',
|
||||
'button',
|
||||
'canvas',
|
||||
'caption',
|
||||
'center',
|
||||
'cite',
|
||||
'code',
|
||||
'col',
|
||||
'colgroup',
|
||||
'content',
|
||||
'data',
|
||||
'datalist',
|
||||
'dd',
|
||||
'decorator',
|
||||
'del',
|
||||
'details',
|
||||
'dfn',
|
||||
'dialog',
|
||||
'dir',
|
||||
'div',
|
||||
'dl',
|
||||
'dt',
|
||||
'element',
|
||||
'em',
|
||||
'fieldset',
|
||||
'figcaption',
|
||||
'figure',
|
||||
'font',
|
||||
'footer',
|
||||
'form',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'head',
|
||||
'header',
|
||||
'hgroup',
|
||||
'hr',
|
||||
'html',
|
||||
'i',
|
||||
'img',
|
||||
'input',
|
||||
'ins',
|
||||
'kbd',
|
||||
'label',
|
||||
'legend',
|
||||
'li',
|
||||
'main',
|
||||
'map',
|
||||
'mark',
|
||||
'marquee',
|
||||
'menu',
|
||||
'menuitem',
|
||||
'meter',
|
||||
'nav',
|
||||
'nobr',
|
||||
'ol',
|
||||
'optgroup',
|
||||
'option',
|
||||
'output',
|
||||
'p',
|
||||
'picture',
|
||||
'pre',
|
||||
'progress',
|
||||
'q',
|
||||
'rp',
|
||||
'rt',
|
||||
'ruby',
|
||||
's',
|
||||
'samp',
|
||||
'section',
|
||||
'select',
|
||||
'shadow',
|
||||
'small',
|
||||
'source',
|
||||
'spacer',
|
||||
'span',
|
||||
'strike',
|
||||
'strong',
|
||||
'style',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'template',
|
||||
'textarea',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'time',
|
||||
'tr',
|
||||
'track',
|
||||
'tt',
|
||||
'u',
|
||||
'ul',
|
||||
'var',
|
||||
'video',
|
||||
'wbr',
|
||||
]);
|
||||
|
||||
// SVG
|
||||
export const svg = freeze([
|
||||
'svg',
|
||||
'a',
|
||||
'altglyph',
|
||||
'altglyphdef',
|
||||
'altglyphitem',
|
||||
'animatecolor',
|
||||
'animatemotion',
|
||||
'animatetransform',
|
||||
'circle',
|
||||
'clippath',
|
||||
'defs',
|
||||
'desc',
|
||||
'ellipse',
|
||||
'filter',
|
||||
'font',
|
||||
'g',
|
||||
'glyph',
|
||||
'glyphref',
|
||||
'hkern',
|
||||
'image',
|
||||
'line',
|
||||
'lineargradient',
|
||||
'marker',
|
||||
'mask',
|
||||
'metadata',
|
||||
'mpath',
|
||||
'path',
|
||||
'pattern',
|
||||
'polygon',
|
||||
'polyline',
|
||||
'radialgradient',
|
||||
'rect',
|
||||
'stop',
|
||||
'style',
|
||||
'switch',
|
||||
'symbol',
|
||||
'text',
|
||||
'textpath',
|
||||
'title',
|
||||
'tref',
|
||||
'tspan',
|
||||
'view',
|
||||
'vkern',
|
||||
]);
|
||||
|
||||
export const svgFilters = freeze([
|
||||
'feBlend',
|
||||
'feColorMatrix',
|
||||
'feComponentTransfer',
|
||||
'feComposite',
|
||||
'feConvolveMatrix',
|
||||
'feDiffuseLighting',
|
||||
'feDisplacementMap',
|
||||
'feDistantLight',
|
||||
'feDropShadow',
|
||||
'feFlood',
|
||||
'feFuncA',
|
||||
'feFuncB',
|
||||
'feFuncG',
|
||||
'feFuncR',
|
||||
'feGaussianBlur',
|
||||
'feImage',
|
||||
'feMerge',
|
||||
'feMergeNode',
|
||||
'feMorphology',
|
||||
'feOffset',
|
||||
'fePointLight',
|
||||
'feSpecularLighting',
|
||||
'feSpotLight',
|
||||
'feTile',
|
||||
'feTurbulence',
|
||||
]);
|
||||
|
||||
// List of SVG elements that are disallowed by default.
|
||||
// We still need to know them so that we can do namespace
|
||||
// checks properly in case one wants to add them to
|
||||
// allow-list.
|
||||
export const svgDisallowed = freeze([
|
||||
'animate',
|
||||
'color-profile',
|
||||
'cursor',
|
||||
'discard',
|
||||
'font-face',
|
||||
'font-face-format',
|
||||
'font-face-name',
|
||||
'font-face-src',
|
||||
'font-face-uri',
|
||||
'foreignobject',
|
||||
'hatch',
|
||||
'hatchpath',
|
||||
'mesh',
|
||||
'meshgradient',
|
||||
'meshpatch',
|
||||
'meshrow',
|
||||
'missing-glyph',
|
||||
'script',
|
||||
'set',
|
||||
'solidcolor',
|
||||
'unknown',
|
||||
'use',
|
||||
]);
|
||||
|
||||
export const mathMl = freeze([
|
||||
'math',
|
||||
'menclose',
|
||||
'merror',
|
||||
'mfenced',
|
||||
'mfrac',
|
||||
'mglyph',
|
||||
'mi',
|
||||
'mlabeledtr',
|
||||
'mmultiscripts',
|
||||
'mn',
|
||||
'mo',
|
||||
'mover',
|
||||
'mpadded',
|
||||
'mphantom',
|
||||
'mroot',
|
||||
'mrow',
|
||||
'ms',
|
||||
'mspace',
|
||||
'msqrt',
|
||||
'mstyle',
|
||||
'msub',
|
||||
'msup',
|
||||
'msubsup',
|
||||
'mtable',
|
||||
'mtd',
|
||||
'mtext',
|
||||
'mtr',
|
||||
'munder',
|
||||
'munderover',
|
||||
'mprescripts',
|
||||
]);
|
||||
|
||||
// Similarly to SVG, we want to know all MathML elements,
|
||||
// even those that we disallow by default.
|
||||
export const mathMlDisallowed = freeze([
|
||||
'maction',
|
||||
'maligngroup',
|
||||
'malignmark',
|
||||
'mlongdiv',
|
||||
'mscarries',
|
||||
'mscarry',
|
||||
'msgroup',
|
||||
'mstack',
|
||||
'msline',
|
||||
'msrow',
|
||||
'semantics',
|
||||
'annotation',
|
||||
'annotation-xml',
|
||||
'mprescripts',
|
||||
'none',
|
||||
]);
|
||||
|
||||
export const text = freeze(['#text']);
|
||||
193
src/ui/static/js/utils/purify/src/utils.js
Normal file
193
src/ui/static/js/utils/purify/src/utils.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
const {
|
||||
entries,
|
||||
setPrototypeOf,
|
||||
isFrozen,
|
||||
getPrototypeOf,
|
||||
getOwnPropertyDescriptor,
|
||||
} = Object;
|
||||
|
||||
let { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports
|
||||
let { apply, construct } = typeof Reflect !== 'undefined' && Reflect;
|
||||
|
||||
if (!freeze) {
|
||||
freeze = function (x) {
|
||||
return x;
|
||||
};
|
||||
}
|
||||
|
||||
if (!seal) {
|
||||
seal = function (x) {
|
||||
return x;
|
||||
};
|
||||
}
|
||||
|
||||
if (!apply) {
|
||||
apply = function (fun, thisValue, args) {
|
||||
return fun.apply(thisValue, args);
|
||||
};
|
||||
}
|
||||
|
||||
if (!construct) {
|
||||
construct = function (Func, args) {
|
||||
return new Func(...args);
|
||||
};
|
||||
}
|
||||
|
||||
const arrayForEach = unapply(Array.prototype.forEach);
|
||||
const arrayIndexOf = unapply(Array.prototype.indexOf);
|
||||
const arrayPop = unapply(Array.prototype.pop);
|
||||
const arrayPush = unapply(Array.prototype.push);
|
||||
const arraySlice = unapply(Array.prototype.slice);
|
||||
|
||||
const stringToLowerCase = unapply(String.prototype.toLowerCase);
|
||||
const stringToString = unapply(String.prototype.toString);
|
||||
const stringMatch = unapply(String.prototype.match);
|
||||
const stringReplace = unapply(String.prototype.replace);
|
||||
const stringIndexOf = unapply(String.prototype.indexOf);
|
||||
const stringTrim = unapply(String.prototype.trim);
|
||||
|
||||
const regExpTest = unapply(RegExp.prototype.test);
|
||||
|
||||
const typeErrorCreate = unconstruct(TypeError);
|
||||
|
||||
/**
|
||||
* Creates a new function that calls the given function with a specified thisArg and arguments.
|
||||
*
|
||||
* @param {Function} func - The function to be wrapped and called.
|
||||
* @returns {Function} A new function that calls the given function with a specified thisArg and arguments.
|
||||
*/
|
||||
function unapply(func) {
|
||||
return (thisArg, ...args) => apply(func, thisArg, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new function that constructs an instance of the given constructor function with the provided arguments.
|
||||
*
|
||||
* @param {Function} func - The constructor function to be wrapped and called.
|
||||
* @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.
|
||||
*/
|
||||
function unconstruct(func) {
|
||||
return (...args) => construct(func, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add properties to a lookup table
|
||||
*
|
||||
* @param {Object} set - The set to which elements will be added.
|
||||
* @param {Array} array - The array containing elements to be added to the set.
|
||||
* @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.
|
||||
* @returns {Object} The modified set with added elements.
|
||||
*/
|
||||
function addToSet(set, array, transformCaseFunc = stringToLowerCase) {
|
||||
if (setPrototypeOf) {
|
||||
// Make 'in' and truthy checks like Boolean(set.constructor)
|
||||
// independent of any properties defined on Object.prototype.
|
||||
// Prevent prototype setters from intercepting set as a this value.
|
||||
setPrototypeOf(set, null);
|
||||
}
|
||||
|
||||
let l = array.length;
|
||||
while (l--) {
|
||||
let element = array[l];
|
||||
if (typeof element === 'string') {
|
||||
const lcElement = transformCaseFunc(element);
|
||||
if (lcElement !== element) {
|
||||
// Config presets (e.g. tags.js, attrs.js) are immutable.
|
||||
if (!isFrozen(array)) {
|
||||
array[l] = lcElement;
|
||||
}
|
||||
|
||||
element = lcElement;
|
||||
}
|
||||
}
|
||||
|
||||
set[element] = true;
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow clone an object
|
||||
*
|
||||
* @param {Object} object - The object to be cloned.
|
||||
* @returns {Object} A new object that copies the original.
|
||||
*/
|
||||
export function clone(object) {
|
||||
const newObject = create(null);
|
||||
|
||||
for (const [property, value] of entries(object)) {
|
||||
if (getOwnPropertyDescriptor(object, property) !== undefined) {
|
||||
newObject[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return newObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method automatically checks if the prop is function or getter and behaves accordingly.
|
||||
*
|
||||
* @param {Object} object - The object to look up the getter function in its prototype chain.
|
||||
* @param {String} prop - The property name for which to find the getter function.
|
||||
* @returns {Function} The getter function found in the prototype chain or a fallback function.
|
||||
*/
|
||||
function lookupGetter(object, prop) {
|
||||
while (object !== null) {
|
||||
const desc = getOwnPropertyDescriptor(object, prop);
|
||||
|
||||
if (desc) {
|
||||
if (desc.get) {
|
||||
return unapply(desc.get);
|
||||
}
|
||||
|
||||
if (typeof desc.value === 'function') {
|
||||
return unapply(desc.value);
|
||||
}
|
||||
}
|
||||
|
||||
object = getPrototypeOf(object);
|
||||
}
|
||||
|
||||
function fallbackValue(element) {
|
||||
console.warn('fallback value for', element);
|
||||
return null;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
export {
|
||||
// Array
|
||||
arrayForEach,
|
||||
arrayIndexOf,
|
||||
arrayPop,
|
||||
arrayPush,
|
||||
arraySlice,
|
||||
// Object
|
||||
entries,
|
||||
freeze,
|
||||
getPrototypeOf,
|
||||
getOwnPropertyDescriptor,
|
||||
isFrozen,
|
||||
setPrototypeOf,
|
||||
seal,
|
||||
create,
|
||||
// RegExp
|
||||
regExpTest,
|
||||
// String
|
||||
stringIndexOf,
|
||||
stringMatch,
|
||||
stringReplace,
|
||||
stringToLowerCase,
|
||||
stringToString,
|
||||
stringTrim,
|
||||
// Errors
|
||||
typeErrorCreate,
|
||||
// Other
|
||||
lookupGetter,
|
||||
addToSet,
|
||||
// Reflect
|
||||
unapply,
|
||||
unconstruct,
|
||||
};
|
||||
|
|
@ -1,239 +1,239 @@
|
|||
class Popover {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("pointerover", (e) => {
|
||||
//POPOVER LOGIC
|
||||
try {
|
||||
if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
|
||||
this.showPopover(e.target);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
window.addEventListener("pointerout", (e) => {
|
||||
//POPOVER LOGIC
|
||||
try {
|
||||
if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
|
||||
this.hidePopover(e.target);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
showPopover(el) {
|
||||
const btn = el.closest("svg");
|
||||
//toggle curr popover
|
||||
const popover = btn.parentElement.querySelector(`[data-popover-content]`);
|
||||
popover.classList.remove("hidden");
|
||||
}
|
||||
|
||||
hidePopover(el) {
|
||||
const btn = el.closest("svg");
|
||||
//toggle curr popover
|
||||
const popover = btn.parentElement.querySelector(`[data-popover-content]`);
|
||||
popover.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
class Tabs {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute("data-tab-handler") ||
|
||||
e.target.closest("button").hasAttribute("data-tab-handler-mobile")
|
||||
) {
|
||||
//get needed data
|
||||
const tab = e.target.closest("button");
|
||||
const tabAtt =
|
||||
tab.getAttribute("data-tab-handler") ||
|
||||
tab.getAttribute("data-tab-handler-mobile");
|
||||
const container = tab.closest("div[data-service-content]");
|
||||
// change style
|
||||
this.resetTabsStyle(container);
|
||||
this.highlightClicked(container, tabAtt);
|
||||
//show content
|
||||
this.hideAllSettings(container);
|
||||
this.showSettingClicked(container, tabAtt);
|
||||
//close dropdown and change btn textcontent on mobile
|
||||
this.setDropBtnText(container, tabAtt);
|
||||
this.closeDropdown(container);
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
try {
|
||||
if (e.target.closest("button").hasAttribute("data-tab-dropdown-btn")) {
|
||||
const dropBtn = e.target.closest("button");
|
||||
const container = dropBtn.closest("div[data-service-content]");
|
||||
this.toggleDropdown(container);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
resetTabsStyle(container) {
|
||||
//reset desktop style
|
||||
const tabsDesktop = container.querySelectorAll("button[data-tab-handler]");
|
||||
tabsDesktop.forEach((tab) => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
//reset mobile style
|
||||
const tabsMobile = container.querySelectorAll(
|
||||
"button[data-tab-handler-mobile]"
|
||||
);
|
||||
tabsMobile.forEach((tab) => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
}
|
||||
|
||||
highlightClicked(container, tabAtt) {
|
||||
//desktop case
|
||||
const tabDesktop = container.querySelector(
|
||||
`button[data-tab-handler='${tabAtt}']`
|
||||
);
|
||||
tabDesktop.classList.add("active");
|
||||
|
||||
//mobile case
|
||||
const tabMobile = container.querySelector(
|
||||
`button[data-tab-handler-mobile='${tabAtt}']`
|
||||
);
|
||||
tabMobile.classList.add("active");
|
||||
}
|
||||
|
||||
hideAllSettings(container) {
|
||||
const plugins = container.querySelectorAll("[data-plugin-item]");
|
||||
plugins.forEach((plugin) => {
|
||||
plugin.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
showSettingClicked(container, tabAtt) {
|
||||
const plugin = container.querySelector(`[data-plugin-item='${tabAtt}']`);
|
||||
plugin.classList.remove("hidden");
|
||||
}
|
||||
|
||||
setDropBtnText(container, tabAtt) {
|
||||
const dropBtn = container.querySelector("[data-tab-dropdown-btn]");
|
||||
dropBtn.querySelector("span").textContent = tabAtt;
|
||||
}
|
||||
|
||||
closeDropdown(container) {
|
||||
const dropdown = container.querySelector("[data-tab-dropdown]");
|
||||
dropdown.classList.add("hidden");
|
||||
dropdown.classList.remove("flex");
|
||||
}
|
||||
|
||||
toggleDropdown(container) {
|
||||
const dropdown = container.querySelector("[data-tab-dropdown]");
|
||||
dropdown.classList.toggle("hidden");
|
||||
dropdown.classList.toggle("flex");
|
||||
}
|
||||
}
|
||||
|
||||
class FormatValue {
|
||||
constructor() {
|
||||
this.inputs = document.querySelectorAll("input");
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.inputs.forEach((inp) => {
|
||||
try {
|
||||
inp.setAttribute("value", inp.getAttribute("value").trim());
|
||||
inp.value = inp.value.trim();
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class FilterSettings {
|
||||
constructor(inputID, container) {
|
||||
this.input = document.querySelector(`input#${inputID}`);
|
||||
//DESKTOP
|
||||
this.container = document.querySelector(container);
|
||||
this.deskTabs = this.container.querySelectorAll(`[data-tab-handler]`);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.input.addEventListener("input", () => {
|
||||
this.resetFilter();
|
||||
//get inp format
|
||||
const inpValue = this.input.value.trim().toLowerCase();
|
||||
//loop all tabs
|
||||
this.deskTabs.forEach((tab) => {
|
||||
//get settings of tabs except multiples
|
||||
const settings = this.getSettingsFromTab(tab);
|
||||
|
||||
//compare total count to currCount to determine
|
||||
//if tabs need to be hidden
|
||||
const settingCount = settings.length;
|
||||
let hiddenCount = 0;
|
||||
settings.forEach((setting) => {
|
||||
try {
|
||||
const title = setting
|
||||
.querySelector("h5")
|
||||
.textContent.trim()
|
||||
.toLowerCase();
|
||||
if (!title.includes(inpValue)) {
|
||||
setting.classList.add("hidden");
|
||||
hiddenCount++;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
//case no setting match, hidden tab and content
|
||||
if (settingCount === hiddenCount) {
|
||||
const tabName = tab.getAttribute(`data-tab-handler`);
|
||||
//hide mobile and desk tabs
|
||||
tab.classList.add("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-tab-handler-mobile="${tabName}"]`)
|
||||
.classList.add("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-plugin-item=${tabName}]`)
|
||||
.querySelector("[data-setting-header]")
|
||||
.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resetFilter() {
|
||||
this.deskTabs.forEach((tab) => {
|
||||
const tabName = tab.getAttribute(`data-tab-handler`);
|
||||
//hide mobile and desk tabs
|
||||
tab.classList.remove("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-tab-handler-mobile="${tabName}"]`)
|
||||
.classList.remove("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-plugin-item=${tabName}]`)
|
||||
.querySelector("[data-setting-header]")
|
||||
.classList.remove("hidden");
|
||||
const settings = this.getSettingsFromTab(tab);
|
||||
settings.forEach((setting) => {
|
||||
setting.classList.remove("hidden");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSettingsFromTab(tabEl) {
|
||||
const tabName = tabEl.getAttribute(`data-tab-handler`);
|
||||
const settingContainer = this.container
|
||||
.querySelector(`[data-plugin-item="${tabName}"]`)
|
||||
.querySelector(`[data-plugin-settings]`);
|
||||
const settings = settingContainer.querySelectorAll(
|
||||
"[data-setting-container]"
|
||||
);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
export { Popover, Tabs, FormatValue, FilterSettings };
|
||||
class Popover {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("pointerover", (e) => {
|
||||
//POPOVER LOGIC
|
||||
try {
|
||||
if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
|
||||
this.showPopover(e.target);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
window.addEventListener("pointerout", (e) => {
|
||||
//POPOVER LOGIC
|
||||
try {
|
||||
if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
|
||||
this.hidePopover(e.target);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
showPopover(el) {
|
||||
const btn = el.closest("svg");
|
||||
//toggle curr popover
|
||||
const popover = btn.parentElement.querySelector(`[data-popover-content]`);
|
||||
popover.classList.remove("hidden");
|
||||
}
|
||||
|
||||
hidePopover(el) {
|
||||
const btn = el.closest("svg");
|
||||
//toggle curr popover
|
||||
const popover = btn.parentElement.querySelector(`[data-popover-content]`);
|
||||
popover.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
class Tabs {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target.closest("button").hasAttribute("data-tab-handler") ||
|
||||
e.target.closest("button").hasAttribute("data-tab-handler-mobile")
|
||||
) {
|
||||
//get needed data
|
||||
const tab = e.target.closest("button");
|
||||
const tabAtt =
|
||||
tab.getAttribute("data-tab-handler") ||
|
||||
tab.getAttribute("data-tab-handler-mobile");
|
||||
const container = tab.closest("div[data-service-content]");
|
||||
// change style
|
||||
this.resetTabsStyle(container);
|
||||
this.highlightClicked(container, tabAtt);
|
||||
//show content
|
||||
this.hideAllSettings(container);
|
||||
this.showSettingClicked(container, tabAtt);
|
||||
//close dropdown and change btn textcontent on mobile
|
||||
this.setDropBtnText(container, tabAtt);
|
||||
this.closeDropdown(container);
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
try {
|
||||
if (e.target.closest("button").hasAttribute("data-tab-dropdown-btn")) {
|
||||
const dropBtn = e.target.closest("button");
|
||||
const container = dropBtn.closest("div[data-service-content]");
|
||||
this.toggleDropdown(container);
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
resetTabsStyle(container) {
|
||||
//reset desktop style
|
||||
const tabsDesktop = container.querySelectorAll("button[data-tab-handler]");
|
||||
tabsDesktop.forEach((tab) => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
//reset mobile style
|
||||
const tabsMobile = container.querySelectorAll(
|
||||
"button[data-tab-handler-mobile]",
|
||||
);
|
||||
tabsMobile.forEach((tab) => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
}
|
||||
|
||||
highlightClicked(container, tabAtt) {
|
||||
//desktop case
|
||||
const tabDesktop = container.querySelector(
|
||||
`button[data-tab-handler='${tabAtt}']`,
|
||||
);
|
||||
tabDesktop.classList.add("active");
|
||||
|
||||
//mobile case
|
||||
const tabMobile = container.querySelector(
|
||||
`button[data-tab-handler-mobile='${tabAtt}']`,
|
||||
);
|
||||
tabMobile.classList.add("active");
|
||||
}
|
||||
|
||||
hideAllSettings(container) {
|
||||
const plugins = container.querySelectorAll("[data-plugin-item]");
|
||||
plugins.forEach((plugin) => {
|
||||
plugin.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
showSettingClicked(container, tabAtt) {
|
||||
const plugin = container.querySelector(`[data-plugin-item='${tabAtt}']`);
|
||||
plugin.classList.remove("hidden");
|
||||
}
|
||||
|
||||
setDropBtnText(container, tabAtt) {
|
||||
const dropBtn = container.querySelector("[data-tab-dropdown-btn]");
|
||||
dropBtn.querySelector("span").textContent = tabAtt;
|
||||
}
|
||||
|
||||
closeDropdown(container) {
|
||||
const dropdown = container.querySelector("[data-tab-dropdown]");
|
||||
dropdown.classList.add("hidden");
|
||||
dropdown.classList.remove("flex");
|
||||
}
|
||||
|
||||
toggleDropdown(container) {
|
||||
const dropdown = container.querySelector("[data-tab-dropdown]");
|
||||
dropdown.classList.toggle("hidden");
|
||||
dropdown.classList.toggle("flex");
|
||||
}
|
||||
}
|
||||
|
||||
class FormatValue {
|
||||
constructor() {
|
||||
this.inputs = document.querySelectorAll("input");
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.inputs.forEach((inp) => {
|
||||
try {
|
||||
inp.setAttribute("value", inp.getAttribute("value").trim());
|
||||
inp.value = inp.value.trim();
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class FilterSettings {
|
||||
constructor(inputID, container) {
|
||||
this.input = document.querySelector(`input#${inputID}`);
|
||||
//DESKTOP
|
||||
this.container = document.querySelector(container);
|
||||
this.deskTabs = this.container.querySelectorAll(`[data-tab-handler]`);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.input.addEventListener("input", () => {
|
||||
this.resetFilter();
|
||||
//get inp format
|
||||
const inpValue = this.input.value.trim().toLowerCase();
|
||||
//loop all tabs
|
||||
this.deskTabs.forEach((tab) => {
|
||||
//get settings of tabs except multiples
|
||||
const settings = this.getSettingsFromTab(tab);
|
||||
|
||||
//compare total count to currCount to determine
|
||||
//if tabs need to be hidden
|
||||
const settingCount = settings.length;
|
||||
let hiddenCount = 0;
|
||||
settings.forEach((setting) => {
|
||||
try {
|
||||
const title = setting
|
||||
.querySelector("h5")
|
||||
.textContent.trim()
|
||||
.toLowerCase();
|
||||
if (!title.includes(inpValue)) {
|
||||
setting.classList.add("hidden");
|
||||
hiddenCount++;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
//case no setting match, hidden tab and content
|
||||
if (settingCount === hiddenCount) {
|
||||
const tabName = tab.getAttribute(`data-tab-handler`);
|
||||
//hide mobile and desk tabs
|
||||
tab.classList.add("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-tab-handler-mobile="${tabName}"]`)
|
||||
.classList.add("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-plugin-item=${tabName}]`)
|
||||
.querySelector("[data-setting-header]")
|
||||
.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resetFilter() {
|
||||
this.deskTabs.forEach((tab) => {
|
||||
const tabName = tab.getAttribute(`data-tab-handler`);
|
||||
//hide mobile and desk tabs
|
||||
tab.classList.remove("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-tab-handler-mobile="${tabName}"]`)
|
||||
.classList.remove("hidden");
|
||||
this.container
|
||||
.querySelector(`[data-plugin-item=${tabName}]`)
|
||||
.querySelector("[data-setting-header]")
|
||||
.classList.remove("hidden");
|
||||
const settings = this.getSettingsFromTab(tab);
|
||||
settings.forEach((setting) => {
|
||||
setting.classList.remove("hidden");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSettingsFromTab(tabEl) {
|
||||
const tabName = tabEl.getAttribute(`data-tab-handler`);
|
||||
const settingContainer = this.container
|
||||
.querySelector(`[data-plugin-item="${tabName}"]`)
|
||||
.querySelector(`[data-plugin-settings]`);
|
||||
const settings = settingContainer.querySelectorAll(
|
||||
"[data-setting-container]",
|
||||
);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
export { Popover, Tabs, FormatValue, FilterSettings };
|
||||
|
|
|
|||
3
src/ui/static/webfonts/bootstrap-icons.css
vendored
3
src/ui/static/webfonts/bootstrap-icons.css
vendored
|
|
@ -1,6 +1,7 @@
|
|||
@font-face {
|
||||
font-family: "bootstrap-icons";
|
||||
src: url("./bootstrap-icons.woff2?524846017b983fc8ded9325d94ed40f3")
|
||||
src:
|
||||
url("./bootstrap-icons.woff2?524846017b983fc8ded9325d94ed40f3")
|
||||
format("woff2"),
|
||||
url("./bootstrap-icons.woff?524846017b983fc8ded9325d94ed40f3")
|
||||
format("woff");
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
|
||||
<script type="module" src="./js/global.js"></script>
|
||||
|
||||
<script src="./js/editor/ace.js"></script>
|
||||
<script async src="./js/utils/purify/purify.min.js"></script>
|
||||
<script async src="./js/editor/ace.js"></script>
|
||||
|
||||
{% if current_endpoint == "global_config" %}
|
||||
<script type="module" src="./js/global_config.js"></script>
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
{% elif current_endpoint == "logs" %}
|
||||
<link rel="stylesheet" type="text/css" href="./css/flatpickr.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./css/flatpickr.dark.css" />
|
||||
<script type="module" src="./js/utils/flatpickr.js"></script>
|
||||
<script async type="module" src="./js/utils/flatpickr.js"></script>
|
||||
<script type="module" src="./js/logs.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
|
|
|
|||
Loading…
Reference in a new issue