Merge pull request #659 from bunkerity/dev

Merge branch "dev" into branch "staging"
This commit is contained in:
Théophile Diot 2023-09-28 12:35:31 +02:00 committed by GitHub
commit 18e5f7bff6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 5280 additions and 4335 deletions

5
.github/codeql.yml vendored
View file

@ -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
View 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}}"

View file

@ -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

View file

@ -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: .

View file

@ -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 }}

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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/*

View file

@ -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 @@
&#124;
👨‍💻 <a href="https://demo.bunkerweb.io">Demo</a>
&#124;
🛡️ <a href="https://github.com/bunkerity/bunkerweb/tree/v1.5.2/examples">Examples</a>
🛡️ <a href="./examples">Examples</a>
&#124;
💬 <a href="https://discord.com/invite/fTf46FmtyD">Chat</a>
&#124;
@ -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.

View file

@ -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);

View file

@ -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

View file

@ -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}`,
);
}
}

View file

@ -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");

View file

@ -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();

View file

@ -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']",
);

View file

@ -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}`,
);
}
}

View file

@ -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;

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -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 };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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',
]);

File diff suppressed because it is too large Load diff

View 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);

View 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']);

View 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,
};

View file

@ -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 };

View file

@ -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");

View file

@ -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"