Merge pull request #363 from TheophileDiot/dev

Advancements on the UI, jobs and database
This commit is contained in:
Théophile Diot 2022-11-19 20:52:26 +01:00 committed by GitHub
commit a0634b5736
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
285 changed files with 11231 additions and 4353 deletions

View File

@ -77,20 +77,25 @@ class Config(ConfigCaller):
)
sleep(5)
# update instances in database
err = self._db.update_instances(self.__instances)
if err:
self.__logger.error(f"Failed to update instances: {err}")
# save config to database
ret = self._db.save_config(self.__config, "autoconf")
if ret:
err = self._db.save_config(self.__config, "autoconf")
if err:
success = False
self.__logger.error(
f"Can't save autoconf config in database: {ret}",
f"Can't save autoconf config in database: {err}",
)
# save custom configs to database
ret = self._db.save_custom_configs(custom_configs, "autoconf")
if ret:
err = self._db.save_custom_configs(custom_configs, "autoconf")
if err:
success = False
self.__logger.error(
f"Can't save autoconf custom configs in database: {ret}",
f"Can't save autoconf custom configs in database: {err}",
)
return success

View File

@ -9,7 +9,7 @@ RUN mkdir -p /usr/share/bunkerweb/deps && \
rm -rf /tmp/req
# Install dependencies
RUN apk add --no-cache --virtual .build-deps g++ gcc libffi-dev && \
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev && \
pip install --no-cache-dir --upgrade pip && \
pip install wheel && \
mkdir -p /usr/share/bunkerweb/deps/python && \

View File

@ -19,7 +19,7 @@ api.global.GET["^/ping$"] = function(api)
end
api.global.POST["^/reload$"] = function(api)
local status = os.execute("/usr/sbin/nginx -s reload")
local status = os.execute("nginx -s reload")
if status == 0 then
return api:response(ngx.HTTP_OK, "success", "reload successful")
end
@ -27,7 +27,7 @@ api.global.POST["^/reload$"] = function(api)
end
api.global.POST["^/stop$"] = function(api)
local status = os.execute("/usr/sbin/nginx -s quit")
local status = os.execute("nginx -s quit")
if status == 0 then
return api:response(ngx.HTTP_OK, "success", "stop successful")
end

View File

@ -108,10 +108,10 @@ try:
external_plugins.append(plugin_file)
if external_plugins:
ret = db.update_external_plugins(external_plugins)
if ret:
err = db.update_external_plugins(external_plugins)
if err:
logger.error(
f"Couldn't update external plugins to database: {ret}",
f"Couldn't update external plugins to database: {err}",
)
except:

View File

@ -1,32 +1,46 @@
#!/usr/bin/python3
import sys, os, traceback
from os import getenv, makedirs
from os.path import exists
from sys import exit as sys_exit, path as sys_path
from traceback import format_exc
sys.path.append("/usr/share/bunkerweb/deps/python")
sys.path.append("/usr/share/bunkerweb/utils")
sys.path.append("/usr/share/bunkerweb/api")
sys_path.append("/usr/share/bunkerweb/deps/python")
sys_path.append("/usr/share/bunkerweb/utils")
sys_path.append("/usr/share/bunkerweb/api")
sys_path.append("/usr/share/bunkerweb/db")
from Database import Database
from logger import setup_logger
from API import API
logger = setup_logger("Lets-encrypt", os.getenv("LOG_LEVEL", "INFO"))
logger = setup_logger("Lets-encrypt", getenv("LOG_LEVEL", "INFO"))
db = Database(
logger,
sqlalchemy_string=getenv("DATABASE_URI", None),
)
status = 0
try:
# Get env vars
is_kubernetes_mode = os.getenv("KUBERNETES_MODE") == "yes"
is_swarm_mode = os.getenv("SWARM_MODE") == "yes"
is_autoconf_mode = os.getenv("AUTOCONF_MODE") == "yes"
token = os.getenv("CERTBOT_TOKEN")
validation = os.getenv("CERTBOT_VALIDATION")
bw_integration = None
if getenv("KUBERNETES_MODE") == "yes":
bw_integration = "Swarm"
elif getenv("SWARM_MODE") == "yes":
bw_integration = "Kubernetes"
elif getenv("AUTOCONF_MODE") == "yes":
bw_integration = "Autoconf"
elif exists("/usr/share/bunkerweb/INTEGRATION"):
with open("/usr/share/bunkerweb/INTEGRATION", "r") as f:
bw_integration = f.read().strip()
token = getenv("CERTBOT_TOKEN")
validation = getenv("CERTBOT_VALIDATION")
# Cluster case
if is_kubernetes_mode or is_swarm_mode or is_autoconf_mode:
for variable, value in os.environ.items():
if not variable.startswith("CLUSTER_INSTANCE_"):
continue
endpoint = value.split(" ")[0]
host = value.split(" ")[1]
if bw_integration in ("Docker", "Swarm", "Kubernetes", "Autoconf"):
for instance in db.get_instances():
endpoint = f"http://{instance['hostname']}:{instance['port']}"
host = instance["server_name"]
api = API(endpoint, host=host)
sent, err, status, resp = api.request(
"POST",
@ -49,15 +63,14 @@ try:
f"Successfully sent API request to {api.get_endpoint()}/lets-encrypt/challenge",
)
# Docker or Linux case
# Linux case
else:
root_dir = "/var/tmp/bunkerweb/lets-encrypt/.well-known/acme-challenge/"
os.makedirs(root_dir, exist_ok=True)
with open(root_dir + token, "w") as f:
makedirs(root_dir, exist_ok=True)
with open(f"{root_dir}{token}", "w") as f:
f.write(validation)
except:
status = 1
logger.error("Exception while running certbot-auth.py :")
print(traceback.format_exc())
logger.error(f"Exception while running certbot-auth.py :\n{format_exc()}")
sys.exit(status)
sys_exit(status)

View File

@ -1,31 +1,45 @@
#!/usr/bin/python3
import sys, os, traceback
from os import getenv, remove
from os.path import exists, isfile
from sys import exit as sys_exit, path as sys_path
from traceback import format_exc
sys.path.append("/usr/share/bunkerweb/deps/python")
sys.path.append("/usr/share/bunkerweb/utils")
sys.path.append("/usr/share/bunkerweb/api")
sys_path.append("/usr/share/bunkerweb/deps/python")
sys_path.append("/usr/share/bunkerweb/utils")
sys_path.append("/usr/share/bunkerweb/api")
sys_path.append("/usr/share/bunkerweb/db")
from Database import Database
from logger import setup_logger
from API import API
logger = setup_logger("Lets-encrypt", os.getenv("LOG_LEVEL", "INFO"))
logger = setup_logger("Lets-encrypt", getenv("LOG_LEVEL", "INFO"))
db = Database(
logger,
sqlalchemy_string=getenv("DATABASE_URI", None),
)
status = 0
try:
# Get env vars
is_kubernetes_mode = os.getenv("KUBERNETES_MODE") == "yes"
is_swarm_mode = os.getenv("SWARM_MODE") == "yes"
is_autoconf_mode = os.getenv("AUTOCONF_MODE") == "yes"
token = os.getenv("CERTBOT_TOKEN")
bw_integration = None
if getenv("KUBERNETES_MODE") == "yes":
bw_integration = "Swarm"
elif getenv("SWARM_MODE") == "yes":
bw_integration = "Kubernetes"
elif getenv("AUTOCONF_MODE") == "yes":
bw_integration = "Autoconf"
elif exists("/usr/share/bunkerweb/INTEGRATION"):
with open("/usr/share/bunkerweb/INTEGRATION", "r") as f:
bw_integration = f.read().strip()
token = getenv("CERTBOT_TOKEN")
# Cluster case
if is_kubernetes_mode or is_swarm_mode or is_autoconf_mode:
for variable, value in os.environ.items():
if not variable.startswith("CLUSTER_INSTANCE_"):
continue
endpoint = value.split(" ")[0]
host = value.split(" ")[1]
if bw_integration in ("Docker", "Swarm", "Kubernetes", "Autoconf"):
for instance in db.get_instances():
endpoint = f"http://{instance['hostname']}:{instance['port']}"
host = instance["server_name"]
api = API(endpoint, host=host)
sent, err, status, resp = api.request(
"DELETE", "/lets-encrypt/challenge", data={"token": token}
@ -45,17 +59,15 @@ try:
logger.info(
f"Successfully sent API request to {api.get_endpoint()}/lets-encrypt/challenge",
)
# Docker or Linux case
# Linux case
else:
challenge_path = (
f"/var/tmp/bunkerweb/lets-encrypt/.well-known/acme-challenge/{token}"
)
if os.path.isfile(challenge_path):
os.remove(challenge_path)
if isfile(challenge_path):
remove(challenge_path)
except:
status = 1
logger.error("Exception while running certbot-cleanup.py :")
print(traceback.format_exc())
logger.error(f"Exception while running certbot-cleanup.py :\n{format_exc()}")
sys.exit(status)
sys_exit(status)

View File

@ -1,7 +1,7 @@
#!/usr/bin/python3
from io import BytesIO
from os import environ, getenv
from os import getenv
from os.path import exists
from subprocess import run, DEVNULL, STDOUT
from sys import exit as sys_exit, path as sys_path
@ -11,13 +11,17 @@ from traceback import format_exc
sys_path.append("/usr/share/bunkerweb/deps/python")
sys_path.append("/usr/share/bunkerweb/utils")
sys_path.append("/usr/share/bunkerweb/api")
sys_path.append("/usr/share/bunkerweb/db")
from docker import DockerClient
from Database import Database
from logger import setup_logger
from API import API
logger = setup_logger("Lets-encrypt", getenv("LOG_LEVEL", "INFO"))
db = Database(
logger,
sqlalchemy_string=getenv("DATABASE_URI", None),
)
status = 0
try:
@ -33,11 +37,11 @@ try:
with open("/usr/share/bunkerweb/INTEGRATION", "r") as f:
bw_integration = f.read().strip()
token = getenv("CERTBOT_TOKEN")
logger.info(f"Certificates renewal for {getenv('RENEWED_DOMAINS')} successful")
# Cluster case
if bw_integration in ("Swarm", "Kubernetes", "Autoconf"):
if bw_integration in ("Docker", "Swarm", "Kubernetes", "Autoconf"):
# Create tarball of /data/cache/letsencrypt
tgz = BytesIO()
with tar_open(mode="w:gz", fileobj=tgz) as tf:
@ -45,11 +49,9 @@ try:
tgz.seek(0, 0)
files = {"archive.tar.gz": tgz}
for variable, value in environ.items():
if not variable.startswith("CLUSTER_INSTANCE_"):
continue
endpoint = value.split(" ")[0]
host = value.split(" ")[1]
for instance in db.get_instances():
endpoint = f"http://{instance['hostname']}:{instance['port']}"
host = instance["server_name"]
api = API(endpoint, host=host)
sent, err, status, resp = api.request(
"POST", "/lets-encrypt/certificates", files=files
@ -85,53 +87,13 @@ try:
logger.info(
f"Successfully sent API request to {api.get_endpoint()}/reload"
)
# Docker or Linux case
elif bw_integration == "Docker":
docker_client = DockerClient(
base_url=getenv("DOCKER_HOST", "unix:///var/run/docker.sock")
# Linux case
else:
proc = run(
["nginx", "-s", "reload"],
stdin=DEVNULL,
stderr=STDOUT,
)
apis = []
for instance in docker_client.containers.list(
filters={"label": "bunkerweb.INSTANCE"}
):
api = None
for var in instance.attrs["Config"]["Env"]:
if var.startswith("API_HTTP_PORT="):
api = API(
f"http://{instance.name}:{var.replace('API_HTTP_PORT=', '', 1)}"
)
break
if api:
apis.append(api)
else:
apis.append(
API(f"http://{instance.name}:{getenv('API_HTTP_PORT', '5000')}")
)
for api in apis:
sent, err, status, resp = api.request("POST", "/reload")
if not sent:
status = 1
logger.error(
f"Can't send API request to {api.get_endpoint()}/reload : {err}"
)
else:
if status != 200:
status = 1
logger.error(
f"Error while sending API request to {api.get_endpoint()}/reload : status = {resp['status']}, msg = {resp['msg']}"
)
else:
logger.info(
f"Successfully sent API request to {api.get_endpoint()}/reload"
)
elif bw_integration == "Linux":
cmd = "/usr/sbin/nginx -s reload"
proc = run(cmd.split(" "), stdin=DEVNULL, stderr=STDOUT)
if proc.returncode != 0:
status = 1
logger.error("Error while reloading nginx")

View File

@ -1,6 +1,6 @@
#!/usr/bin/python3
from os import environ, getenv
from os import environ, getcwd, getenv
from os.path import exists
from subprocess import DEVNULL, STDOUT, run
from sys import exit as sys_exit, path as sys_path
@ -8,17 +8,42 @@ from traceback import format_exc
sys_path.append("/usr/share/bunkerweb/deps/python")
sys_path.append("/usr/share/bunkerweb/utils")
sys_path.append("/usr/share/bunkerweb/db")
from Database import Database
from logger import setup_logger
logger = setup_logger("LETS-ENCRYPT", getenv("LOG_LEVEL", "INFO"))
db = Database(
logger,
sqlalchemy_string=getenv("DATABASE_URI", None),
)
status = 0
def certbot_new(domains, email):
cmd = f"/usr/share/bunkerweb/deps/python/bin/certbot certonly --manual --preferred-challenges=http --manual-auth-hook /usr/share/bunkerweb/core/letsencrypt/jobs/certbot-auth.py --manual-cleanup-hook /usr/share/bunkerweb/core/letsencrypt/jobs/certbot-cleanup.py -n -d {domains} --email {email} --agree-tos"
if getenv("USE_LETS_ENCRYPT_STAGING") == "yes":
cmd += " --staging"
environ["PYTHONPATH"] = "/usr/share/bunkerweb/deps/python"
proc = run(
cmd.split(" "),
[
"/usr/share/bunkerweb/deps/python/bin/certbot certonly",
"--manual",
"--preferred-challenges=http",
"--manual-auth-hook",
f"{getcwd()}/certbot-auth.py",
"--manual-cleanup-hook",
f"{getcwd()}/certbot-cleanup.py",
"-n",
"-d",
domains,
"--email",
email,
"--agree-tos",
"--logs-dir",
"/var/tmp/bunkerweb",
"--work-dir",
"/var/lib/bunkerweb",
]
+ (["--staging"] if getenv("USE_LETS_ENCRYPT_STAGING", "no") == "yes" else []),
stdin=DEVNULL,
stderr=STDOUT,
env=environ,
@ -26,34 +51,37 @@ def certbot_new(domains, email):
return proc.returncode
logger = setup_logger("LETS-ENCRYPT", getenv("LOG_LEVEL", "INFO"))
status = 0
try:
# Multisite case
if getenv("MULTISITE") == "yes":
for first_server in getenv("SERVER_NAME").split(" "):
if getenv("MULTISITE", "no") == "yes":
for first_server in getenv("SERVER_NAME", "").split(" "):
if (
getenv(f"{first_server}_AUTO_LETS_ENCRYPT", getenv("AUTO_LETS_ENCRYPT"))
not first_server
or getenv(
f"{first_server}_AUTO_LETS_ENCRYPT",
getenv("AUTO_LETS_ENCRYPT", "no"),
)
!= "yes"
):
continue
if first_server == "":
continue
real_server_name = getenv(f"{first_server}_SERVER_NAME", first_server)
domains = real_server_name.replace(" ", ",")
domains = getenv(f"{first_server}_SERVER_NAME", first_server).replace(
" ", ","
)
if exists(f"/etc/letsencrypt/live/{first_server}/cert.pem"):
logger.info(
f"Certificates already exists for domain(s) {domains}",
)
continue
real_email = getenv(
f"{first_server}_EMAIL_LETS_ENCRYPT",
getenv("EMAIL_LETS_ENCRYPT", f"contact@{first_server}"),
)
if real_email == "":
if not real_email:
real_email = f"contact@{first_server}"
logger.info(
f"Asking certificates for domains : {domains} (email = {real_email}) ...",
)
@ -67,16 +95,32 @@ try:
f"Certificate generation succeeded for domain(s) : {domains}"
)
if exists(f"/etc/letsencrypt/live/{first_server}/cert.pem"):
with open(f"/etc/letsencrypt/live/{first_server}/cert.pem") as f:
cert = f.read()
# Update db
err = db.update_job_cache(
"letsencrypt",
first_server,
"cert.pem",
cert,
)
if err:
logger.warning(f"Couldn't update db cache: {err}")
# Singlesite case
elif getenv("AUTO_LETS_ENCRYPT") == "yes" and getenv("SERVER_NAME") != "":
first_server = getenv("SERVER_NAME").split(" ")[0]
domains = getenv("SERVER_NAME").replace(" ", ",")
elif getenv("AUTO_LETS_ENCRYPT", "no") == "yes" and getenv("SERVER_NAME", ""):
first_server = getenv("SERVER_NAME", "").split(" ")[0]
domains = getenv("SERVER_NAME", "").replace(" ", ",")
if exists(f"/etc/letsencrypt/live/{first_server}/cert.pem"):
logger.info(f"Certificates already exists for domain(s) {domains}")
else:
real_email = getenv("EMAIL_LETS_ENCRYPT", f"contact@{first_server}")
if real_email == "":
if not real_email:
real_email = f"contact@{first_server}"
logger.info(
f"Asking certificates for domain(s) : {domains} (email = {real_email}) ...",
)
@ -88,7 +132,19 @@ try:
f"Certificate generation succeeded for domain(s) : {domains}"
)
if exists(f"/etc/letsencrypt/live/{first_server}/cert.pem"):
with open(f"/etc/letsencrypt/live/{first_server}/cert.pem") as f:
cert = f.read()
# Update db
err = db.update_job_cache(
"letsencrypt",
first_server,
"cert.pem",
cert,
)
if err:
logger.warning(f"Couldn't update db cache: {err}")
except:
status = 1
logger.error(f"Exception while running certbot-new.py :\n{format_exc()}")

View File

@ -1,6 +1,6 @@
#!/usr/bin/python3
from os import environ, getenv
from os import environ, getcwd, getenv
from os.path import exists
from subprocess import DEVNULL, STDOUT, run
from sys import exit as sys_exit, path as sys_path
@ -13,10 +13,16 @@ from logger import setup_logger
def renew(domain):
cmd = f"/usr/share/bunkerweb/deps/python/bin/certbot renew --cert-name {domain} --deploy-hook /usr/share/bunkerweb/core/letsencrypt/jobs/certbot-deploy.py"
environ["PYTHONPATH"] = "/usr/share/bunkerweb/deps/python"
proc = run(
cmd.split(" "),
[
"/usr/share/bunkerweb/deps/python/bin/certbot",
"renew",
"--cert-name",
domain,
"--deploy-hook",
f"{getcwd()}/certbot-deploy.py",
],
stdin=DEVNULL,
stderr=STDOUT,
env=environ,
@ -30,23 +36,25 @@ status = 0
try:
if getenv("MULTISITE") == "yes":
for first_server in getenv("SERVER_NAME").split(" "):
if first_server == "":
continue
if (
getenv(f"{first_server}_AUTO_LETS_ENCRYPT", getenv("AUTO_LETS_ENCRYPT"))
not first_server
or getenv(
f"{first_server}_AUTO_LETS_ENCRYPT",
getenv("AUTO_LETS_ENCRYPT", "no"),
)
!= "yes"
or not exists(f"/etc/letsencrypt/live/{first_server}/cert.pem")
):
continue
if not exists(f"/etc/letsencrypt/live/{first_server}/cert.pem"):
continue
ret = renew(first_server)
if ret != 0:
status = 2
logger.error(
f"Certificates renewal for {first_server} failed",
)
elif getenv("AUTO_LETS_ENCRYPT") == "yes" and getenv("SERVER_NAME") != "":
first_server = getenv("SERVER_NAME").split(" ")[0]
elif getenv("AUTO_LETS_ENCRYPT", "no") == "yes" and not getenv("SERVER_NAME", ""):
first_server = getenv("SERVER_NAME", "").split(" ")[0]
if exists(f"/etc/letsencrypt/live/{first_server}/cert.pem"):
ret = renew(first_server)
if ret != 0:

View File

@ -2,7 +2,12 @@ from contextlib import contextmanager
from copy import deepcopy
from datetime import datetime
from hashlib import sha256
from logging import INFO, WARNING, Logger, getLogger
from logging import (
NOTSET,
Logger,
_levelToName,
_nameToLevel,
)
import oracledb
from os import _exit, getenv, listdir, makedirs
from os.path import dirname, exists
@ -24,6 +29,8 @@ from traceback import format_exc
from model import (
Base,
Instances,
Logs,
Plugins,
Settings,
Global_values,
@ -55,10 +62,6 @@ class Database:
self.__sql_session = None
self.__sql_engine = None
getLogger("sqlalchemy.engine").setLevel(
logger.level if logger.level != INFO else WARNING
)
if not sqlalchemy_string:
sqlalchemy_string = getenv(
"DATABASE_URI", "sqlite:////var/lib/bunkerweb/db.sqlite3"
@ -77,7 +80,6 @@ class Database:
sqlalchemy_string,
encoding="utf-8",
future=True,
logging_name="sqlalchemy.engine",
)
except ArgumentError:
self.__logger.error(f"Invalid database URI: {sqlalchemy_string}")
@ -1238,7 +1240,6 @@ class Database:
{
"service_id": cache.service_id,
"file_name": cache.file_name,
"data": cache.data,
"last_update": cache.last_update.strftime(
"%Y/%m/%d, %I:%M:%S %p"
),
@ -1247,7 +1248,6 @@ class Database:
.with_entities(
Jobs_cache.service_id,
Jobs_cache.file_name,
Jobs_cache.data,
Jobs_cache.last_update,
)
.filter_by(job_name=job.name)
@ -1266,3 +1266,104 @@ class Database:
.all()
)
}
def get_job_cache_file(self, job_name: str, file_name: str) -> Optional[bytes]:
"""Get job cache file."""
with self.__db_session() as session:
return (
session.query(Jobs_cache)
.with_entities(Jobs_cache.data)
.filter_by(job_name=job_name, file_name=file_name)
.first()
)
def save_log(
self,
log: str,
level: Tuple[str, int],
component: str,
) -> str:
"""Save log."""
with self.__db_session() as session:
session.add(
Logs(
id=int(datetime.now().timestamp()),
message=log,
level=str(_levelToName[_nameToLevel.get(level, NOTSET)])
if isinstance(level, str)
else _levelToName.get(level, "NOTSET"),
component=component,
)
)
try:
session.commit()
except BaseException:
return format_exc()
return ""
def add_instance(self, hostname: str, port: int, server_name: str) -> str:
"""Add instance."""
with self.__db_session() as session:
db_instance = (
session.query(Instances)
.with_entities(Instances.hostname)
.filter_by(hostname=hostname)
.first()
)
if db_instance is not None:
return "An instance with the same hostname already exists."
session.add(
Instances(hostname=hostname, port=int(port), server_name=server_name)
)
try:
session.commit()
except BaseException:
return f"An error occurred while adding the instance {hostname} (port: {port}, server name: {server_name}).\n{format_exc()}"
return ""
def update_instances(self, instances: List[Dict[str, Any]]) -> str:
"""Update instances."""
to_put = []
with self.__db_session() as session:
session.query(Instances).delete()
for instance in instances:
to_put.append(
Instances(
hostname=instance["hostname"],
port=instance["env"].get("API_HTTP_PORT", 5000),
server_name=instance["env"].get("API_SERVER_NAME", "bwapi"),
)
)
try:
session.add_all(to_put)
session.commit()
except BaseException:
return format_exc()
return ""
def get_instances(self) -> List[Dict[str, Any]]:
"""Get instances."""
with self.__db_session() as session:
return [
{
"hostname": instance.hostname,
"port": instance.port,
"server_name": instance.server_name,
}
for instance in (
session.query(Instances)
.with_entities(
Instances.hostname, Instances.port, Instances.server_name
)
.all()
)
]

View File

@ -10,7 +10,6 @@ from sqlalchemy import (
PrimaryKeyConstraint,
SmallInteger,
String,
text,
TIMESTAMP,
)
from sqlalchemy.orm import declarative_base, relationship
@ -30,7 +29,15 @@ CUSTOM_CONFIGS_TYPES_ENUM = Enum(
"stream_http",
name="custom_configs_types_enum",
)
LOG_LEVELS_ENUM = Enum("DEBUG", "INFO", "WARNING", "ERROR", name="log_levels_enum")
LOG_LEVELS_ENUM = Enum(
"CRITICAL",
"ERROR",
"WARNING",
"INFO",
"DEBUG",
"NOTSET",
name="log_levels_enum",
)
INTEGRATIONS_ENUM = Enum(
"Linux",
"Docker",
@ -265,6 +272,14 @@ class Logs(Base):
component = Column(String(255), nullable=False)
class Instances(Base):
__tablename__ = "instances"
hostname = Column(String(255), primary_key=True)
port = Column(Integer, nullable=False)
server_name = Column(String(255), nullable=False)
class Metadata(Base):
__tablename__ = "metadata"

View File

@ -174,8 +174,7 @@ if __name__ == "__main__":
retries += 1
sleep(5)
cmd = "/usr/sbin/nginx -s reload"
proc = run(cmd.split(" "), stdin=DEVNULL, stderr=STDOUT)
proc = run(["nginx", "-s", "reload"], stdin=DEVNULL, stderr=STDOUT)
if proc.returncode != 0:
status = 1
logger.error("Error while reloading nginx")

View File

@ -369,6 +369,25 @@ if __name__ == "__main__":
sys_exit(1)
else:
logger.info("Config successfully saved to database")
if apis:
for api in apis:
endpoint_data = api.get_endpoint().replace("http://", "").split(":")
err = db.add_instance(
endpoint_data[0], endpoint_data[1], api.get_host()
)
if err:
logger.warning(err)
else:
err = db.add_instance(
"localhost",
config_files.get("API_HTTP_PORT", 5000),
config_files.get("API_SERVER_NAME", "bwapi"),
)
if err:
logger.warning(err)
except SystemExit as e:
sys_exit(e)
except:

View File

@ -11,7 +11,7 @@ log "ENTRYPOINT" "" "Starting BunkerWeb v$(cat /usr/share/bunkerweb/VERSIO
function trap_exit() {
log "ENTRYPOINT" "" "Catched stop operation"
log "ENTRYPOINT" "" "Stopping nginx ..."
/usr/sbin/nginx -s stop
nginx -s stop
}
trap "trap_exit" TERM INT QUIT

View File

@ -1,38 +1,89 @@
import logging
from logging import (
CRITICAL,
DEBUG,
ERROR,
INFO,
WARNING,
Logger,
_levelToName,
_nameToLevel,
addLevelName,
basicConfig,
getLogger,
setLoggerClass,
)
from os import getenv
from threading import Lock
logging.basicConfig(
class BWLogger(Logger):
def __init__(self, name, level=INFO):
self.name = name
self.db_lock = Lock()
return super(BWLogger, self).__init__(name, level)
def _log(
self,
level,
msg,
args,
exc_info=None,
extra=None,
stack_info=False,
stacklevel=1,
*,
store_db: bool = False,
db=None,
):
if store_db is True and db is not None:
with self.db_lock:
err = db.save_log(msg, level, self.name)
if err:
self.error(f"Failed to save log to database: {err}")
return super(BWLogger, self)._log(
level, msg, args, exc_info, extra, stack_info, stacklevel
)
setLoggerClass(BWLogger)
default_level = _nameToLevel.get(getenv("LOG_LEVEL", "INFO").upper(), INFO)
basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="[%Y-%m-%d %H:%M:%S]",
level=logging.INFO,
level=default_level,
)
getLogger("sqlalchemy.orm.mapper.Mapper").setLevel(
default_level if default_level != INFO else WARNING
)
getLogger("sqlalchemy.orm.relationships.RelationshipProperty").setLevel(
default_level if default_level != INFO else WARNING
)
getLogger("sqlalchemy.orm.strategies.LazyLoader").setLevel(
default_level if default_level != INFO else WARNING
)
getLogger("sqlalchemy.pool.impl.QueuePool").setLevel(
default_level if default_level != INFO else WARNING
)
getLogger("sqlalchemy.engine.Engine").setLevel(
default_level if default_level != INFO else WARNING
)
# Edit the default levels of the logging module
logging.addLevelName(logging.CRITICAL, "🚨")
logging.addLevelName(logging.DEBUG, "🐛")
logging.addLevelName(logging.ERROR, "")
logging.addLevelName(logging.INFO, " ")
logging.addLevelName(logging.WARNING, "⚠️ ")
addLevelName(CRITICAL, "🚨")
addLevelName(DEBUG, "🐛")
addLevelName(ERROR, "")
addLevelName(INFO, " ")
addLevelName(WARNING, "⚠️ ")
def setup_logger(title: str, level=logging.INFO) -> logging.Logger:
def setup_logger(title: str, level=INFO) -> Logger:
"""Set up local logger"""
title = title.upper()
logger = logging.getLogger(title)
if level not in (
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.CRITICAL,
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL",
):
level = logging.INFO
logger.setLevel(level)
logger = getLogger(title)
logger.setLevel(_nameToLevel.get(level, _levelToName.get(level, INFO)))
return logger

View File

@ -10,7 +10,7 @@ RUN mkdir -p /usr/share/bunkerweb/deps && \
rm -rf /tmp/req
# Install python requirements
RUN apk add --no-cache --virtual .build-deps g++ gcc libffi-dev && \
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev && \
pip install --no-cache-dir --upgrade pip && \
pip install wheel && \
mkdir -p /usr/share/bunkerweb/deps/python && \
@ -51,7 +51,8 @@ RUN apk add --no-cache bash libgcc libstdc++ openssl && \
chown -R root:scheduler /usr/share/bunkerweb /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb && \
for dir in $(echo "/usr/share/bunkerweb /etc/bunkerweb") ; do find ${dir} -type f -exec chmod 0740 {} \; ; done && \
for dir in $(echo "/usr/share/bunkerweb /etc/bunkerweb") ; do find ${dir} -type d -exec chmod 0750 {} \; ; done && \
chmod 770 /var/cache/bunkerweb /var/lib/bunkerweb /var/tmp/bunkerweb && \
chmod -R 770 /var/cache/bunkerweb /var/lib/bunkerweb /var/tmp/bunkerweb && \
find /usr/share/bunkerweb/core/*/jobs/* -type f -exec chmod 750 {} \; && \
chmod 750 /usr/share/bunkerweb/gen/*.py /usr/share/bunkerweb/scheduler/main.py /usr/share/bunkerweb/scheduler/entrypoint.sh /usr/share/bunkerweb/helpers/*.sh /usr/share/bunkerweb/deps/python/bin/* && \
mkdir /etc/nginx && \
chown -R scheduler:scheduler /etc/nginx && \

View File

@ -80,7 +80,7 @@ class JobScheduler(ApiCaller):
if self.__integration == "Linux":
self.__logger.info("Reloading nginx ...")
proc = run(
["/usr/sbin/nginx", "-s", "reload"],
["nginx", "-s", "reload"],
stdin=DEVNULL,
stderr=PIPE,
env=self.__env,
@ -111,6 +111,8 @@ class JobScheduler(ApiCaller):
stdin=DEVNULL,
stderr=STDOUT,
env=self.__env,
user=101,
group=101,
)
except BaseException:
with self.__thread_lock:

View File

@ -152,7 +152,6 @@ if __name__ == "__main__":
logger,
sqlalchemy_string=getenv("DATABASE_URI", None),
)
custom_configs = db.get_custom_configs()
# END Define db because otherwhise it will be undefined for Linux
logger.info("Scheduler started ...")
@ -192,11 +191,11 @@ if __name__ == "__main__":
"Kubernetes",
"Autoconf",
):
ret = db.set_autoconf_load(False)
if ret:
err = db.set_autoconf_load(False)
if err:
success = False
logger.error(
f"Can't set autoconf loaded metadata to false in database: {ret}",
f"Can't set autoconf loaded metadata to false in database: {err}",
)
while not db.is_autoconf_loaded():
@ -209,8 +208,16 @@ if __name__ == "__main__":
or db.get_config() != dotenv_values("/var/tmp/bunkerweb/variables.env")
):
# run the config saver
cmd = f"python /usr/share/bunkerweb/gen/save_config.py --settings /usr/share/bunkerweb/settings.json"
proc = subprocess_run(cmd.split(" "), stdin=DEVNULL, stderr=STDOUT)
proc = subprocess_run(
[
"python",
"/usr/share/bunkerweb/gen/save_config.py",
"--settings",
"/usr/share/bunkerweb/settings.json",
],
stdin=DEVNULL,
stderr=STDOUT,
)
if proc.returncode != 0:
logger.error(
"Config saver failed, configuration will not work as expected...",
@ -261,10 +268,10 @@ if __name__ == "__main__":
if custom_confs:
old_configs = db.get_custom_configs()
ret = db.save_custom_configs(custom_confs, "manual")
if ret:
err = db.save_custom_configs(custom_confs, "manual")
if err:
logger.error(
f"Couldn't save some manually created custom configs to database: {ret}",
f"Couldn't save some manually created custom configs to database: {err}",
)
custom_configs = db.get_custom_configs()
@ -273,7 +280,7 @@ if __name__ == "__main__":
generate_custom_configs(custom_configs, integration, api_caller)
logger.info("Executing scheduler ...")
generate = not exists(
"/var/tmp/bunkerweb/variables.env"
) or env != dotenv_values("/var/tmp/bunkerweb/variables.env")
@ -300,8 +307,21 @@ if __name__ == "__main__":
if generate is True:
# run the generator
cmd = f"python /usr/share/bunkerweb/gen/main.py --settings /usr/share/bunkerweb/settings.json --templates /usr/share/bunkerweb/confs --output /etc/nginx{f' --variables {args.variables}' if args.variables else ''}"
proc = subprocess_run(cmd.split(" "), stdin=DEVNULL, stderr=STDOUT)
proc = subprocess_run(
[
"python3",
"/usr/share/bunkerweb/gen/main.py",
"--settings",
"/usr/share/bunkerweb/settings.json",
"--templates",
"/usr/share/bunkerweb/confs",
"--output",
"/etc/nginx",
]
+ (["--variables", args.variables] if args.variables else []),
stdin=DEVNULL,
stderr=STDOUT,
)
if proc.returncode != 0:
logger.error(
@ -351,13 +371,12 @@ if __name__ == "__main__":
logger.info("Reloading nginx ...")
# Reloading the nginx server.
# Had to use this instead of the nginx reload command because it was not working
proc = subprocess_run(["nginx", "-s", "reload"], stdin=DEVNULL, stderr=STDOUT)
# proc = run(
# ["/usr/sbin/nginx", "-s", "reload"],
# stdin=DEVNULL,
# stderr=PIPE,
# env=deepcopy(env),
# )
proc = subprocess_run(
["nginx", "-s", "reload"],
stdin=DEVNULL,
stderr=STDOUT,
env=deepcopy(env),
)
if proc.returncode == 0:
logger.info("Successfuly reloaded nginx")
else:

View File

@ -10,7 +10,7 @@ RUN mkdir -p /usr/share/bunkerweb/deps && \
rm -rf /tmp/req
# Install python requirements
RUN apk add --no-cache --virtual .build-deps g++ gcc && \
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev && \
pip install --no-cache-dir --upgrade pip && \
pip install wheel && \
mkdir -p /usr/share/bunkerweb/deps/python && \

View File

@ -1,3 +1,4 @@
from io import BytesIO
from bs4 import BeautifulSoup
from copy import deepcopy
from datetime import datetime, timedelta, timezone
@ -15,6 +16,7 @@ from flask import (
redirect,
render_template,
request,
send_file,
url_for,
)
from flask_login import LoginManager, login_required, login_user, logout_user
@ -49,12 +51,6 @@ from src.User import User
from utils import (
check_settings,
env_to_summary_class,
form_plugin_gen,
form_service_gen,
form_service_gen_multiple,
form_service_gen_multiple_values,
gen_folders_tree_html,
get_variables,
path_to_dict,
)
@ -138,6 +134,8 @@ elif integration == "Kubernetes":
kubernetes_client = kube_client.CoreV1Api()
db = Database(logger)
with open("/usr/share/bunkerweb/VERSION", "r") as f:
bw_version = f.read().strip()
try:
app.config.update(
@ -164,14 +162,6 @@ except FileNotFoundError as e:
sys_exit(1)
# Declare functions for jinja2
app.jinja_env.globals.update(env_to_summary_class=env_to_summary_class)
app.jinja_env.globals.update(form_plugin_gen=form_plugin_gen)
app.jinja_env.globals.update(form_service_gen=form_service_gen)
app.jinja_env.globals.update(form_service_gen_multiple=form_service_gen_multiple)
app.jinja_env.globals.update(
form_service_gen_multiple_values=form_service_gen_multiple_values
)
app.jinja_env.globals.update(gen_folders_tree_html=gen_folders_tree_html)
app.jinja_env.globals.update(check_settings=check_settings)
@ -273,27 +263,30 @@ def home():
posts: a list of posts
"""
r = get(
"https://raw.githubusercontent.com/bunkerity/bunkerweb/master/VERSION",
)
try:
r = get(
"https://raw.githubusercontent.com/bunkerity/bunkerweb/master/VERSION",
)
except BaseException:
r = None
remote_version = None
if r.status_code == 200:
if r and r.status_code == 200:
remote_version = r.text.strip()
with open("/usr/share/bunkerweb/VERSION", "r") as f:
version = f.read().strip()
headers = default_headers()
headers.update({"User-Agent": "bunkerweb-ui"})
r = get(
"https://www.bunkerity.com/wp-json/wp/v2/posts",
headers=headers,
)
try:
r = get(
"https://www.bunkerity.com/wp-json/wp/v2/posts",
headers=headers,
)
except BaseException:
r = None
formatted_posts = None
if r.status_code == 200:
if r and r.status_code == 200:
posts = r.json()
formatted_posts = []
@ -325,12 +318,13 @@ def home():
return render_template(
"home.html",
check_version=not remote_version or version == remote_version,
check_version=not remote_version or bw_version == remote_version,
remote_version=remote_version,
version=version,
version=bw_version,
instances_number=instances_number,
services_number=services_number,
posts=formatted_posts,
plugins_errors=db.get_plugins_errors(),
dark_mode=app.config["DARK_MODE"],
)
@ -1019,7 +1013,6 @@ def plugins():
return render_template(
"plugins.html",
plugins=app.config["CONFIG"].get_plugins(),
dark_mode=app.config["DARK_MODE"],
)
@ -1275,36 +1268,48 @@ def logs_linux():
@app.route("/logs/<container_id>", methods=["GET"])
@login_required
def logs_container(container_id):
last_update = request.args.get(
"last_update",
str(datetime.now().timestamp() - timedelta(days=1).total_seconds()),
)
last_update = request.args.get("last_update", None)
from_date = request.args.get("from_date", None)
to_date = request.args.get("to_date", None)
if from_date is not None:
last_update = from_date
if any(arg and not arg.isdigit() for arg in [last_update, from_date, to_date]):
return (
jsonify(
{
"status": "ko",
"message": "arguments must all be integers (timestamps)",
}
),
422,
)
elif not last_update:
last_update = int(
datetime.now().timestamp()
- timedelta(days=1).total_seconds() # 1 day before
)
else:
last_update = int(last_update) // 1000
to_date = int(to_date) // 1000 if to_date else None
logs = []
if docker_client:
try:
if last_update and not last_update.isdigit():
return (
jsonify(
{
"status": "ko",
"message": "last_update must be an integer",
}
),
422,
)
if getenv("SWARM_MODE", "no") == "no":
docker_logs = docker_client.containers.get(container_id).logs(
stdout=True,
stderr=True,
since=datetime.fromtimestamp(int(last_update)),
since=datetime.fromtimestamp(last_update),
timestamps=True,
)
else:
docker_logs = docker_client.services.get(container_id).logs(
stdout=True,
stderr=True,
since=datetime.fromtimestamp(int(last_update)),
since=datetime.fromtimestamp(last_update),
timestamps=True,
)
@ -1324,7 +1329,7 @@ def logs_container(container_id):
kubernetes_logs = kubernetes_client.read_namespaced_pod_log(
container_id,
getenv("KUBERNETES_NAMESPACE", "default"),
since_seconds=int(datetime.now().timestamp() - int(last_update)),
since_seconds=int(datetime.now().timestamp() - last_update),
timestamps=True,
)
tmp_logs = kubernetes_logs.split("\n")[0:-1]
@ -1339,10 +1344,16 @@ def logs_container(container_id):
404,
)
logger.warning(tmp_logs)
for log in tmp_logs:
splitted = log.split(" ")
timestamp = splitted[0]
if to_date is not None and dateutil_parse(timestamp).timestamp() > to_date:
break
log = " ".join(splitted[1:])
log_lower = log.lower()
logs.append(
{
"content": log,
@ -1371,11 +1382,47 @@ def logs_container(container_id):
def jobs():
return render_template(
"jobs.html",
jobs=db.get_jobs(),
jobs=dumps(db.get_jobs()),
dark_mode=app.config["DARK_MODE"],
)
@app.route("/jobs/download", methods=["GET"])
@login_required
def jobs_download():
job_name = request.args.get("job_name", None)
file_name = request.args.get("file_name", None)
if not job_name or not file_name:
return (
jsonify(
{
"status": "ko",
"message": "job_name and file_name are required",
}
),
422,
)
cache_file = db.get_job_cache_file(job_name, file_name)
if not cache_file:
return (
jsonify(
{
"status": "ko",
"message": "file not found",
}
),
404,
)
with BytesIO(cache_file) as file:
file.seek(0)
return send_file(file, as_attachment=True, attachment_filename=file_name)
@app.route("/login", methods=["GET", "POST"])
def login():
fail = False
@ -1416,12 +1463,6 @@ def darkmode():
return jsonify({"status": "ok"})
@app.route("/plugins_errors", methods=["GET"])
@login_required
def plugins_errors():
return jsonify({"status": "ok", "plugins_errors": db.get_plugins_errors()})
@app.route("/check_reloading")
@login_required
def check_reloading():

View File

@ -166,10 +166,10 @@ class Config:
if proc.returncode != 0:
raise Exception(f"Error from generator (return code = {proc.returncode})")
ret = self.__db.save_config(conf, "ui")
if ret:
err = self.__db.save_config(conf, "ui")
if err:
self.__logger.error(
f"Can't save config in database: {ret}",
f"Can't save config in database: {err}",
)
def get_plugins_settings(self) -> dict:

View File

@ -43,9 +43,9 @@ class ConfigFiles:
}
)
ret = self.__db.save_custom_configs(custom_configs, "ui")
if ret:
self.__logger.error(f"Could not save custom configs: {ret}")
err = self.__db.save_custom_configs(custom_configs, "ui")
if err:
self.__logger.error(f"Could not save custom configs: {err}")
return "Couldn't save custom configs to database"
return ""

File diff suppressed because one or more lines are too long

View File

@ -807,6 +807,10 @@ h6 {
z-index: 50;
}
.-z-10 {
z-index: -10;
}
.order-2 {
order: 2;
}
@ -831,10 +835,6 @@ h6 {
grid-column: span 2 / span 2;
}
.col-span-4 {
grid-column: span 4 / span 4;
}
.col-span-9 {
grid-column: span 9 / span 9;
}
@ -912,11 +912,6 @@ h6 {
margin-bottom: 0.25rem;
}
.my-auto {
margin-top: auto;
margin-bottom: auto;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
@ -927,9 +922,9 @@ h6 {
margin-right: 0.625rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
.mx-4 {
margin-left: 1rem;
margin-right: 1rem;
}
.my-3 {
@ -937,14 +932,19 @@ h6 {
margin-bottom: 0.75rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.my-5 {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.mx-4 {
margin-left: 1rem;
margin-right: 1rem;
.my-6 {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.mb-1 {
@ -1019,10 +1019,18 @@ h6 {
margin-top: 0.25rem;
}
.ml-6 {
margin-left: 1.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-0 {
margin-top: 0px;
}
@ -1047,14 +1055,6 @@ h6 {
margin-top: 1.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mb-auto {
margin-bottom: auto;
}
.mr-6 {
margin-right: 1.5rem;
}
@ -1071,14 +1071,14 @@ h6 {
margin-left: -1.75rem;
}
.ml-6 {
margin-left: 1.5rem;
}
.mb-5 {
margin-bottom: 1.25rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.box-content {
box-sizing: content-box;
}
@ -1195,6 +1195,18 @@ h6 {
max-height: 25rem;
}
.max-h-60 {
max-height: 15rem;
}
.max-h-16 {
max-height: 4rem;
}
.max-h-80 {
max-height: 20rem;
}
.max-h-90 {
max-height: 22.5rem;
}
@ -1203,6 +1215,14 @@ h6 {
max-height: 2rem;
}
.max-h-30 {
max-height: 7.5rem;
}
.max-h-135 {
max-height: 33.75rem;
}
.min-h-20 {
min-height: 5rem;
}
@ -1223,8 +1243,12 @@ h6 {
min-height: 13rem;
}
.min-h-75-screen {
min-height: 75vh;
.min-h-\[55vh\] {
min-height: 55vh;
}
.min-h-12 {
min-height: 3rem;
}
.min-h-75 {
@ -1235,6 +1259,18 @@ h6 {
min-height: 85vh;
}
.min-h-screen {
min-height: 100vh;
}
.min-h-90 {
min-height: 22.5rem;
}
.min-h-80 {
min-height: 20rem;
}
.w-full {
width: 100%;
}
@ -1307,6 +1343,26 @@ h6 {
min-width: 0px;
}
.min-w-\[900\] {
min-width: 900;
}
.min-w-\[900px\] {
min-width: 900px;
}
.min-w-\[800px\] {
min-width: 800px;
}
.min-w-\[700px\] {
min-width: 700px;
}
.min-w-\[750px\] {
min-width: 750px;
}
.max-w-screen-sm {
max-width: 576px;
}
@ -1319,6 +1375,10 @@ h6 {
max-width: 10rem;
}
.max-w-120 {
max-width: 30rem;
}
.max-w-64 {
max-width: 16rem;
}
@ -1335,6 +1395,22 @@ h6 {
max-width: 300px;
}
.max-w-60 {
max-width: 15rem;
}
.max-w-\[460px\] {
max-width: 460px;
}
.max-w-\[40px\] {
max-width: 40px;
}
.max-w-\[400px\] {
max-width: 400px;
}
.flex-auto {
flex: 1 1 auto;
}
@ -1487,6 +1563,10 @@ h6 {
resize: both;
}
.scroll-m-4 {
scroll-margin: 1rem;
}
.list-none {
list-style-type: none;
}
@ -1509,10 +1589,6 @@ h6 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
@ -1570,11 +1646,6 @@ h6 {
row-gap: 0.5rem;
}
.gap-x-1 {
-moz-column-gap: 0.25rem;
column-gap: 0.25rem;
}
.overflow-auto {
overflow: auto;
}
@ -1583,6 +1654,14 @@ h6 {
overflow: hidden;
}
.overflow-scroll {
overflow: scroll;
}
.overflow-x-auto {
overflow-x: auto;
}
.overflow-y-auto {
overflow-y: auto;
}
@ -1591,6 +1670,14 @@ h6 {
overflow-x: hidden;
}
.overflow-x-scroll {
overflow-x: scroll;
}
.overflow-y-scroll {
overflow-y: scroll;
}
.whitespace-nowrap {
white-space: nowrap;
}
@ -1816,6 +1903,10 @@ h6 {
--tw-bg-opacity: 0;
}
.bg-gradient-to-tl {
background-image: linear-gradient(to top left, var(--tw-gradient-stops));
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
@ -1824,10 +1915,6 @@ h6 {
background-image: none;
}
.bg-gradient-to-tl {
background-image: linear-gradient(to top left, var(--tw-gradient-stops));
}
.from-transparent {
--tw-gradient-from: transparent;
--tw-gradient-to: rgb(0 0 0 / 0);
@ -1951,6 +2038,10 @@ h6 {
fill: #047857;
}
.fill-amber-500 {
fill: #f59e0b;
}
.fill-slate-800 {
fill: #3a416f;
}
@ -1988,6 +2079,10 @@ h6 {
padding: 1.5rem;
}
.p-12 {
padding: 3rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@ -2073,6 +2168,16 @@ h6 {
padding-bottom: 1.25rem;
}
.px-12 {
padding-left: 3rem;
padding-right: 3rem;
}
.py-16 {
padding-top: 4rem;
padding-bottom: 4rem;
}
.pb-0 {
padding-bottom: 0px;
}
@ -2109,10 +2214,6 @@ h6 {
padding-bottom: 0.5rem;
}
.pt-5 {
padding-top: 1.25rem;
}
.pl-6 {
padding-left: 1.5rem;
}
@ -2149,6 +2250,10 @@ h6 {
padding-left: 1.75rem;
}
.pt-5 {
padding-top: 1.25rem;
}
.text-left {
text-align: left;
}
@ -2717,6 +2822,10 @@ h6 {
background-color: rgb(251 177 64 / 0.8);
}
.hover\:bg-primary\/80:hover {
background-color: rgb(8 85 119 / 0.8);
}
.hover\:bg-primary\/30:hover {
background-color: rgb(8 85 119 / 0.3);
}
@ -2730,10 +2839,6 @@ h6 {
background-color: rgb(248 249 250 / var(--tw-bg-opacity));
}
.hover\:bg-primary\/80:hover {
background-color: rgb(8 85 119 / 0.8);
}
.hover\:opacity-80:hover {
opacity: 0.8;
}
@ -3065,10 +3170,23 @@ h6 {
grid-column: span 1 / span 1;
}
.sm\:col-span-2 {
grid-column: span 2 / span 2;
}
.sm\:col-span-8 {
grid-column: span 8 / span 8;
}
.sm\:col-start-5 {
grid-column-start: 5;
}
.sm\:my-0 {
margin-top: 0px;
margin-bottom: 0px;
}
.sm\:mx-6 {
margin-left: 1.5rem;
margin-right: 1.5rem;
@ -3087,6 +3205,10 @@ h6 {
margin-right: 4rem;
}
.sm\:block {
display: block;
}
.sm\:hidden {
display: none;
}
@ -3107,6 +3229,22 @@ h6 {
max-height: 7rem;
}
.sm\:max-h-0 {
max-height: 0px;
}
.sm\:max-h-120 {
max-height: 30rem;
}
.sm\:max-h-135 {
max-height: 33.75rem;
}
.sm\:max-h-125 {
max-height: 31.25rem;
}
.sm\:w-80 {
width: 20rem;
}
@ -3170,6 +3308,18 @@ h6 {
grid-column: span 6 / span 6;
}
.md\:col-span-5 {
grid-column: span 5 / span 5;
}
.md\:col-span-7 {
grid-column: span 7 / span 7;
}
.md\:col-span-1 {
grid-column: span 1 / span 1;
}
.md\:my-0 {
margin-top: 0px;
margin-bottom: 0px;
@ -3209,10 +3359,18 @@ h6 {
display: none;
}
.md\:max-h-160 {
max-height: 40rem;
}
.md\:min-h-75-screen {
min-height: 75vh;
}
.md\:min-h-50-screen {
min-height: 50vh;
}
.md\:w-1\/2 {
width: 50%;
}
@ -3221,6 +3379,11 @@ h6 {
justify-content: flex-end;
}
.md\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.md\:py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
@ -3248,6 +3411,10 @@ h6 {
}
@media (min-width: 992px) {
.lg\:relative {
position: relative;
}
.lg\:order-1 {
order: 1;
}
@ -3268,16 +3435,16 @@ h6 {
grid-column: span 12 / span 12;
}
.lg\:col-span-9 {
grid-column: span 9 / span 9;
.lg\:col-span-8 {
grid-column: span 8 / span 8;
}
.lg\:col-span-3 {
grid-column: span 3 / span 3;
}
.lg\:col-span-8 {
grid-column: span 8 / span 8;
.lg\:col-span-1 {
grid-column: span 1 / span 1;
}
.lg\:mx-8 {
@ -3298,6 +3465,18 @@ h6 {
margin-bottom: 0px;
}
.lg\:block {
display: block;
}
.lg\:flex {
display: flex;
}
.lg\:hidden {
display: none;
}
.lg\:h-9 {
height: 2.25rem;
}
@ -3342,14 +3521,25 @@ h6 {
justify-content: space-between;
}
.lg\:justify-items-start {
justify-items: start;
}
.lg\:gap-6 {
gap: 1.5rem;
}
.lg\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.lg\:bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(235 239 244 / var(--tw-bg-opacity));
}
.lg\:bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(248 249 250 / var(--tw-bg-opacity));
}
.lg\:text-left {
text-align: left;
}
@ -3452,4 +3642,13 @@ h6 {
.\33xl\:col-span-4 {
grid-column: span 4 / span 4;
}
.\33xl\:col-span-2 {
grid-column: span 2 / span 2;
}
}
.\[\&\>\*\]\:bg-primary>* {
--tw-bg-opacity: 1;
background-color: rgb(8 85 119 / var(--tw-bg-opacity));
}

View File

@ -0,0 +1,275 @@
.datepicker {
display: none;
}
.datepicker.active {
display: block;
}
.datepicker-dropdown {
position: absolute;
top: 0;
left: 0;
z-index: 10;
padding-top: 4px;
}
.datepicker-dropdown.datepicker-orient-top {
padding-top: 0;
padding-bottom: 4px;
}
.datepicker-picker {
display: inline-block;
border-radius: 0;
background-color: #fefefe;
}
.datepicker-dropdown .datepicker-picker {
box-shadow: 0 0 0 1px #cacaca;
}
.datepicker-picker span {
display: block;
flex: 1;
border: 0;
border-radius: 0;
cursor: default;
text-align: center;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.datepicker-main {
padding: 2px;
}
.datepicker-footer {
box-shadow: inset 0 1px 1px rgba(10, 10, 10, 0.1);
background-color: #e6e6e6;
}
.datepicker-grid,
.datepicker-view .days-of-week,
.datepicker-view,
.datepicker-controls {
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 {
box-shadow: inset 0 -1px 1px rgba(10, 10, 10, 0.1);
background-color: #e6e6e6;
padding: 0.375rem 0.75rem;
text-align: center;
font-weight: bold;
}
.datepicker-header .datepicker-controls {
padding: 2px 2px 0;
}
.datepicker-controls .button {
margin: 0;
background-color: #fefefe;
color: #0a0a0a;
}
.datepicker-controls .button:hover,
.datepicker-controls .button:focus {
background-color: #d8d8d8;
}
.datepicker-controls .button:hover[disabled],
.datepicker-controls .button:focus[disabled] {
opacity: 0.25;
background-color: #fefefe;
color: #0a0a0a;
}
.datepicker-header .datepicker-controls .button {
border-color: transparent;
font-weight: bold;
}
.datepicker-footer .datepicker-controls .button {
margin: calc(0.375rem - 1px) 0.375rem;
border-radius: 0;
width: 100%;
font-size: 0.75rem;
}
.datepicker-controls .view-switch {
flex: auto;
}
.datepicker-controls .prev-btn,
.datepicker-controls .next-btn {
padding-right: 0.375rem;
padding-left: 0.375rem;
width: 2.25rem;
}
.datepicker-controls .prev-btn.disabled,
.datepicker-controls .next-btn.disabled {
visibility: hidden;
}
.datepicker-view .dow {
height: 1.5rem;
line-height: 1.5rem;
font-size: 0.875rem;
font-weight: bold;
}
.datepicker-view .week {
width: 2.25rem;
color: #8a8a8a;
font-size: 0.75rem;
}
@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.prev:not(.disabled),
.datepicker-cell.next:not(.disabled) {
color: #cacaca;
}
.datepicker-cell.prev.selected,
.datepicker-cell.next.selected {
color: #e5e5e5;
}
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today) {
border-radius: 0;
background-color: #f7f7f7;
}
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover {
background-color: #f1f1f1;
}
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused {
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-start {
border-radius: 0 0 0 0;
}
.datepicker-cell.range-end {
border-radius: 0 0 0 0;
}
.datepicker-cell.range {
border-radius: 0;
background-color: #e6e6e6;
}
.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:focus,
.datepicker-input.in-edit:active {
box-shadow: 0 0 0.25em 0.25em rgba(164, 164, 164, 0.2);
}

View File

@ -0,0 +1 @@
.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)}

View File

@ -0,0 +1,318 @@
.datepicker {
display: none;
}
.datepicker.active {
display: block;
}
.datepicker-dropdown {
position: absolute;
top: 0;
left: 0;
z-index: 20;
padding-top: 4px;
}
.datepicker-dropdown.datepicker-orient-top {
padding-top: 0;
padding-bottom: 4px;
}
.datepicker-picker {
display: inline-block;
border-radius: 4px;
background-color: white;
}
.datepicker-dropdown .datepicker-picker {
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
}
.datepicker-picker span {
display: block;
flex: 1;
border: 0;
border-radius: 4px;
cursor: default;
text-align: center;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.datepicker-main {
padding: 2px;
}
.datepicker-footer {
box-shadow: inset 0 1px 1px rgba(10, 10, 10, 0.1);
background-color: whitesmoke;
}
.datepicker-grid,
.datepicker-view .days-of-week,
.datepicker-view,
.datepicker-controls {
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 {
box-shadow: inset 0 -1px 1px rgba(10, 10, 10, 0.1);
background-color: whitesmoke;
padding: 0.375rem 0.75rem;
text-align: center;
font-weight: 700;
}
.datepicker-header .datepicker-controls {
padding: 2px 2px 0;
}
.datepicker-controls .button {
display: inline-flex;
position: relative;
align-items: center;
justify-content: center;
margin: 0;
border: 1px solid #dbdbdb;
border-radius: 4px;
box-shadow: none;
background-color: white;
cursor: pointer;
padding: calc(0.375em - 1px) 0.75em;
height: 2.25em;
vertical-align: top;
text-align: center;
line-height: 1.5;
white-space: nowrap;
color: #363636;
font-size: 1rem;
}
.datepicker-controls .button:focus,
.datepicker-controls .button:active {
outline: none;
}
.datepicker-controls .button:hover {
border-color: #b5b5b5;
color: #363636;
}
.datepicker-controls .button:focus {
border-color: #3273dc;
color: #363636;
}
.datepicker-controls .button:focus:not(:active) {
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25);
}
.datepicker-controls .button:active {
border-color: #4a4a4a;
color: #363636;
}
.datepicker-controls .button[disabled] {
cursor: not-allowed;
}
.datepicker-header .datepicker-controls .button {
border-color: transparent;
font-weight: bold;
}
.datepicker-header .datepicker-controls .button:hover {
background-color: #f9f9f9;
}
.datepicker-header .datepicker-controls .button:focus:not(:active) {
box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25);
}
.datepicker-header .datepicker-controls .button:active {
background-color: #f2f2f2;
}
.datepicker-header .datepicker-controls .button[disabled] {
box-shadow: none;
}
.datepicker-footer .datepicker-controls .button {
margin: calc(0.375rem - 1px) 0.375rem;
border-radius: 2px;
width: 100%;
font-size: 0.75rem;
}
.datepicker-controls .view-switch {
flex: auto;
}
.datepicker-controls .prev-btn,
.datepicker-controls .next-btn {
padding-right: 0.375rem;
padding-left: 0.375rem;
width: 2.25rem;
}
.datepicker-controls .prev-btn.disabled,
.datepicker-controls .next-btn.disabled {
visibility: hidden;
}
.datepicker-view .dow {
height: 1.5rem;
line-height: 1.5rem;
font-size: 0.875rem;
font-weight: 700;
}
.datepicker-view .week {
width: 2.25rem;
color: #b5b5b5;
font-size: 0.75rem;
}
@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: #f9f9f9;
cursor: pointer;
}
.datepicker-cell.focused:not(.selected) {
background-color: #e8e8e8;
}
.datepicker-cell.selected,
.datepicker-cell.selected:hover {
background-color: #3273dc;
color: #fff;
font-weight: 600;
}
.datepicker-cell.disabled {
color: #dbdbdb;
}
.datepicker-cell.prev:not(.disabled),
.datepicker-cell.next:not(.disabled) {
color: #7a7a7a;
}
.datepicker-cell.prev.selected,
.datepicker-cell.next.selected {
color: #e6e6e6;
}
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today) {
border-radius: 0;
background-color: whitesmoke;
}
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover {
background-color: #eeeeee;
}
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused {
background-color: #e8e8e8;
}
.datepicker-cell.today:not(.selected) {
background-color: #00d1b2;
}
.datepicker-cell.today:not(.selected):not(.disabled) {
color: #fff;
}
.datepicker-cell.today.focused:not(.selected) {
background-color: #00c4a7;
}
.datepicker-cell.range-end:not(.selected),
.datepicker-cell.range-start:not(.selected) {
background-color: #b5b5b5;
color: #fff;
}
.datepicker-cell.range-end.focused:not(.selected),
.datepicker-cell.range-start.focused:not(.selected) {
background-color: #afafaf;
}
.datepicker-cell.range-start {
border-radius: 4px 0 0 4px;
}
.datepicker-cell.range-end {
border-radius: 0 4px 4px 0;
}
.datepicker-cell.range {
border-radius: 0;
background-color: #dbdbdb;
}
.datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover {
background-color: #d5d5d5;
}
.datepicker-cell.range.disabled {
color: #c2c2c2;
}
.datepicker-cell.range.focused {
background-color: #cfcfcf;
}
.datepicker-view.datepicker-grid .datepicker-cell {
height: 4.5rem;
line-height: 4.5rem;
}
.datepicker-input.in-edit {
border-color: #2366d1;
}
.datepicker-input.in-edit:focus,
.datepicker-input.in-edit:active {
box-shadow: 0 0 0.25em 0.25em rgba(35, 102, 209, 0.2);
}

1
src/ui/static/css/datepicker.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.dropzone,.dropzone *{box-sizing:border-box}.dropzone{position:relative}.dropzone .dz-preview{position:relative;display:inline-block;width:120px;margin:.5em}.dropzone .dz-preview .dz-progress{display:block;height:15px;border:1px solid #aaa}.dropzone .dz-preview .dz-progress .dz-upload{display:block;height:100%;width:0;background:green}.dropzone .dz-preview .dz-error-message{color:red;display:none}.dropzone .dz-preview.dz-error .dz-error-message,.dropzone .dz-preview.dz-error .dz-error-mark{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{display:block}.dropzone .dz-preview .dz-error-mark,.dropzone .dz-preview .dz-success-mark{position:absolute;display:none;left:30px;top:30px;width:54px;height:58px;left:50%;margin-left:-27px}/*# sourceMappingURL=basic.css.map */

View File

@ -1 +1,274 @@
@keyframes passing-through{0%{opacity:0;transform:translateY(40px)}30%,70%{opacity:1;transform:translateY(0px)}100%{opacity:0;transform:translateY(-40px)}}@keyframes slide-in{0%{opacity:0;transform:translateY(40px)}30%{opacity:1;transform:translateY(0px)}}@keyframes pulse{0%{transform:scale(1)}10%{transform:scale(1.1)}20%{transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:1px solid rgba(0,0,0,.8);border-radius:5px;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:.5}.dropzone .dz-message{text-align:center;margin:3em 0}.dropzone .dz-message .dz-button{background:none;color:inherit;border:none;padding:0;font:inherit;cursor:pointer;outline:inherit}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:#fff}.dropzone .dz-preview.dz-image-preview .dz-details{transition:opacity .2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,.8);background-color:rgba(255,255,255,.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,.4);padding:0 .4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{transform:scale(1.05, 1.05);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px;background:rgba(0,0,0,.8);border-radius:50%}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px;fill:#fff}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;transition:all .2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;transition:opacity .4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:20px;top:50%;margin-top:-10px;left:15%;right:15%;border:3px solid rgba(0,0,0,.8);background:rgba(0,0,0,.8);border-radius:10px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#fff;display:block;position:relative;height:100%;width:0;transition:width 300ms ease-in-out;border-radius:17px}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;transition:opacity .3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#b10606;padding:.5em 1em;color:#fff}.dropzone .dz-preview .dz-error-message:after{content:"";position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #b10606}/*# sourceMappingURL=dropzone.css.map */
/*# sourceMappingURL=basic.css.map */
@keyframes passing-through {
0% {
opacity: 0;
transform: translateY(40px);
}
30%,
70% {
opacity: 1;
transform: translateY(0px);
}
100% {
opacity: 0;
transform: translateY(-40px);
}
}
@keyframes slide-in {
0% {
opacity: 0;
transform: translateY(40px);
}
30% {
opacity: 1;
transform: translateY(0px);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
10% {
transform: scale(1.1);
}
20% {
transform: scale(1);
}
}
.dropzone,
.dropzone * {
box-sizing: border-box;
}
.dropzone {
min-height: 100px;
border: 1px solid rgba(0, 0, 0, 0.8);
border-radius: 5px;
padding: 0;
}
.dropzone.dz-clickable {
cursor: pointer;
}
.dropzone.dz-clickable * {
cursor: default;
}
.dropzone.dz-clickable .dz-message,
.dropzone.dz-clickable .dz-message * {
cursor: pointer;
}
.dropzone.dz-started .dz-message {
display: none;
}
.dropzone.dz-drag-hover {
border-style: solid;
}
.dropzone.dz-drag-hover .dz-message {
opacity: 0.5;
}
.dropzone .dz-message {
text-align: center;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.dropzone .dz-message .dz-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}
.dropzone .dz-preview {
position: relative;
display: inline-block;
vertical-align: top;
margin: 12px;
min-height: 100px;
}
.dropzone .dz-preview:hover {
z-index: 1000;
}
.dropzone .dz-preview:hover .dz-details {
opacity: 1;
}
.dropzone .dz-preview.dz-file-preview .dz-image {
border-radius: 20px;
background: #999;
background: linear-gradient(to bottom, #eee, #ddd);
}
.dropzone .dz-preview.dz-file-preview .dz-details {
opacity: 1;
}
.dropzone .dz-preview.dz-image-preview {
background: #fff;
}
.dropzone .dz-preview.dz-image-preview .dz-details {
transition: opacity 0.2s linear;
}
.dropzone .dz-preview .dz-remove {
font-size: 14px;
text-align: center;
display: block;
cursor: pointer;
border: none;
}
.dropzone .dz-preview .dz-remove:hover {
text-decoration: underline;
}
.dropzone .dz-preview:hover .dz-details {
opacity: 1;
}
.dropzone .dz-preview .dz-details {
z-index: 20;
position: absolute;
top: 0;
left: 0;
opacity: 0;
font-size: 13px;
min-width: 100%;
max-width: 100%;
padding: 2em 1em;
text-align: center;
color: rgba(0, 0, 0, 0.9);
line-height: 150%;
}
.dropzone .dz-preview .dz-details .dz-size {
margin-bottom: 1em;
font-size: 16px;
}
.dropzone .dz-preview .dz-details .dz-filename {
white-space: nowrap;
}
.dropzone .dz-preview .dz-details .dz-filename:hover span {
border: 1px solid rgba(200, 200, 200, 0.8);
background-color: rgba(255, 255, 255, 0.8);
}
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
overflow: hidden;
text-overflow: ellipsis;
}
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
border: 1px solid transparent;
}
.dropzone .dz-preview .dz-details .dz-filename span,
.dropzone .dz-preview .dz-details .dz-size span {
background-color: rgba(255, 255, 255, 0.4);
padding: 0 0.4em;
border-radius: 3px;
}
.dropzone .dz-preview:hover .dz-image img {
transform: scale(1.05, 1.05);
filter: blur(8px);
}
.dropzone .dz-preview .dz-image {
border-radius: 20px;
overflow: hidden;
width: 120px;
height: 120px;
position: relative;
display: block;
z-index: 10;
}
.dropzone .dz-preview .dz-image img {
display: block;
}
.dropzone .dz-preview.dz-success .dz-success-mark {
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
}
.dropzone .dz-preview.dz-error .dz-error-mark {
opacity: 1;
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
}
.dropzone .dz-preview .dz-success-mark,
.dropzone .dz-preview .dz-error-mark {
pointer-events: none;
opacity: 0;
z-index: 500;
position: absolute;
display: block;
top: 50%;
left: 50%;
margin-left: -27px;
margin-top: -27px;
background: rgba(0, 0, 0, 0.8);
border-radius: 50%;
}
.dropzone .dz-preview .dz-success-mark svg,
.dropzone .dz-preview .dz-error-mark svg {
display: block;
width: 54px;
height: 54px;
fill: #fff;
}
.dropzone .dz-preview.dz-processing .dz-progress {
opacity: 1;
transition: all 0.2s linear;
}
.dropzone .dz-preview.dz-complete .dz-progress {
opacity: 0;
transition: opacity 0.4s ease-in;
}
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
animation: pulse 6s ease infinite;
}
.dropzone .dz-preview .dz-progress {
opacity: 1;
z-index: 1000;
pointer-events: none;
position: absolute;
height: 20px;
top: 50%;
margin-top: -10px;
left: 15%;
right: 15%;
border: 3px solid rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
overflow: hidden;
}
.dropzone .dz-preview .dz-progress .dz-upload {
background: #fff;
display: block;
position: relative;
height: 100%;
width: 0;
transition: width 300ms ease-in-out;
border-radius: 17px;
}
.dropzone .dz-preview.dz-error .dz-error-message {
display: block;
}
.dropzone .dz-preview.dz-error:hover .dz-error-message {
opacity: 1;
pointer-events: auto;
}
.dropzone .dz-preview .dz-error-message {
pointer-events: none;
z-index: 1000;
position: absolute;
display: block;
display: none;
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 8px;
font-size: 13px;
top: 130px;
left: -10px;
width: 140px;
background: #b10606;
padding: 0.5em 1em;
color: #fff;
}
.dropzone .dz-preview .dz-error-message:after {
content: "";
position: absolute;
top: -6px;
left: 64px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #b10606;
} /*# sourceMappingURL=dropzone.css.map */

View File

@ -1,146 +0,0 @@
declare type AirDatepickerSelector = string | HTMLElement;
export declare type AirDatepickerLocale = {
days: string[],
daysShort: string[],
daysMin: string[],
months: string[],
monthsShort: string[],
today: string,
clear: string,
dateFormat: string,
timeFormat: string,
firstDay: 0 | 1 | 2 | 3 | 4 | 5 | 6,
}
export declare type AirDatepickerButton = {
content: string | ((dp: AirDatepicker) => string),
tagName?: keyof HTMLElementTagNameMap,
className?: string,
attrs?: Record<string, string>,
onClick?: (dp: AirDatepicker) => void
}
export declare type AirDatepickerButtonPresets = 'clear' | 'today';
export declare type AirDatepickerPosition = 'left' | 'left top' | 'left bottom' | 'top' | 'top left' | 'top right' | 'right' | 'right top' | 'right bottom' | 'bottom' | 'bottom left' | 'bottom right';
export declare type AirDatepickerViews = 'days' | 'months' | 'years';
export declare type AirDatepickerViewsSingle = 'day' | 'month' | 'year';
export declare type AirDatepickerDate = string | number | Date;
export declare type AirDatepickerNavEntry = string | ((dp: AirDatepicker) => string);
export declare type AirDatepickerDecade = [number, number];
export declare type AirDatepickerPositionCallback = (
{
$datepicker,
$target,
$pointer,
isViewChange,
done
}: {
$datepicker: HTMLDivElement,
$target: HTMLInputElement,
$pointer: HTMLElement,
isViewChange: boolean,
done: () => void
}) => void | (() => void)
export declare type AirDatepickerOptions = {
classes: string
inline: boolean,
locale: Partial<AirDatepickerLocale>,
startDate: AirDatepickerDate,
firstDay: number,
isMobile: boolean,
visible: boolean,
weekends: [number, number],
dateFormat: string | ((d: Date) => string),
altField: AirDatepickerSelector,
altFieldDateFormat: string,
toggleSelected: boolean,
keyboardNav: boolean,
selectedDates: AirDatepickerDate[] | false,
container: AirDatepickerSelector,
position: AirDatepickerPosition | AirDatepickerPositionCallback,
offset: number,
view: AirDatepickerViews,
minView: AirDatepickerViews,
showOtherMonths: boolean,
selectOtherMonths: boolean,
moveToOtherMonthsOnSelect: boolean,
showOtherYears: boolean,
selectOtherYears: boolean,
moveToOtherYearsOnSelect: boolean,
minDate: AirDatepickerDate | false,
maxDate: AirDatepickerDate | false,
disableNavWhenOutOfRange: true,
multipleDates: number | true | false,
multipleDatesSeparator: string,
range: boolean,
dynamicRange: boolean,
buttons: AirDatepickerButtonPresets | AirDatepickerButton | (AirDatepickerButtonPresets| AirDatepickerButton)[] | false,
monthsField: keyof AirDatepickerLocale,
showEvent: string,
autoClose: boolean,
prevHtml: string,
nextHtml: string,
navTitles: {
days?: AirDatepickerNavEntry,
months?: AirDatepickerNavEntry,
years?: AirDatepickerNavEntry
},
timepicker: boolean,
onlyTimepicker: boolean,
dateTimeSeparator: string,
timeFormat: string,
minHours: number,
maxHours: number,
minMinutes: number,
maxMinutes: number,
hoursStep: number,
minutesStep: number,
onSelect: ({date, formattedDate, datepicker}: {date: Date | Date[], formattedDate: string | string[], datepicker: AirDatepicker}) => void,
onChangeViewDate: ({month, year, decade}: {month: number, year: number, decade: AirDatepickerDecade}) => void,
onChangeView: (view: AirDatepickerViews) => void,
onRenderCell: (params: {date: Date, cellType: AirDatepickerViewsSingle, datepicker: AirDatepicker}) => ({
disabled?: boolean,
classes?: string,
html?: string
attrs?: Record<string, string | number | undefined>
} | void),
onShow: (isAnimationComplete: boolean) => void,
onHide: (isAnimationComplete: boolean) => void,
onClickDayName: ({dayIndex, datepicker}: {dayIndex: number, datepicker: AirDatepicker}) => void
}
declare class AirDatepicker<E extends HTMLElement = HTMLInputElement> {
constructor(el: string | E, opts? : Partial<AirDatepickerOptions>)
static version: string
show: () => void
hide: () => void
next: () => void
prev: () => void
selectDate: (date: AirDatepickerDate | AirDatepickerDate[], opts?: {updateTime?: boolean, silent?: boolean}) => void
unselectDate: (date: AirDatepickerDate) => void
clear: () => void
formatDate: (date: AirDatepickerDate, format: string) => string
destroy: () => void
update: (newOpts: Partial<AirDatepickerOptions>) => void
setCurrentView: (newView: AirDatepickerViews) => void
setViewDate: (newViewDate: AirDatepickerDate) => void
setFocusDate: (date: AirDatepickerDate | false, opts?: {viewDateTransition?: boolean}) => void
up: (date?: AirDatepickerDate) => void
down: (date?: AirDatepickerDate) => void
$el: E
$datepicker: HTMLDivElement
viewDate: Date
currentView: AirDatepickerViews
selectedDates: Date[]
focusDate: Date | false
visible: boolean
}
export default AirDatepicker;

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
import AirDatepicker from "air-datepicker";
export default AirDatepicker;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/ar' {
import {AirDatepickerLocale} from 'air-datepicker';
const ar: AirDatepickerLocale;
export default ar;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['الأحد', 'الأثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعه', 'السبت'],
daysShort: ['الأحد', 'الأثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعه', 'السبت'],
daysMin: ['الأحد', 'الأثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعه', 'السبت'],
months: ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'اكتوبر', 'نوفمبر', 'ديسمبر'],
monthsShort: ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'اكتوبر', 'نوفمبر', 'ديسمبر'],
today: 'اليوم',
clear: 'حذف',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'hh:mm aa',
firstDay: 0
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/cs' {
import {AirDatepickerLocale} from 'air-datepicker';
const cs: AirDatepickerLocale;
export default cs;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Neděle', 'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota'],
daysShort: ['Ne', 'Po', 'Út', 'St', 'Čt', 'Pá', 'So'],
daysMin: ['Ne', 'Po', 'Út', 'St', 'Čt', 'Pá', 'So'],
months: ['Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen', 'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec'],
monthsShort: ['Led', 'Úno', 'Bře', 'Dub', 'Kvě', 'Čvn', 'Čvc', 'Srp', 'Zář', 'Říj', 'Lis', 'Pro'],
today: 'Dnes',
clear: 'Vymazat',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/da' {
import {AirDatepickerLocale} from 'air-datepicker';
const da: AirDatepickerLocale;
export default da;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag'],
daysShort: ['Søn', 'Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør'],
daysMin: ['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø'],
months: ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
today: 'I dag',
clear: 'Nulstil',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/de' {
import {AirDatepickerLocale} from 'air-datepicker';
const de: AirDatepickerLocale;
export default de;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
daysShort: ['Son', 'Mon', 'Die', 'Mit', 'Don', 'Fre', 'Sam'],
daysMin: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
monthsShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
today: 'Heute',
clear: 'Aufräumen',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'HH:ii',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/en' {
import {AirDatepickerLocale} from 'air-datepicker';
const en: AirDatepickerLocale;
export default en;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
today: 'Today',
clear: 'Clear',
dateFormat: 'MM/dd/yyyy',
timeFormat: 'hh:mm aa',
firstDay: 0
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/es' {
import {AirDatepickerLocale} from 'air-datepicker';
const es: AirDatepickerLocale;
export default es;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'],
daysShort: ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'],
daysMin: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa'],
months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
today: 'Hoy',
clear: 'Limpiar',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'hh:mm aa',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/fi' {
import {AirDatepickerLocale} from 'air-datepicker';
const fi: AirDatepickerLocale;
export default fi;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Sunnuntai', 'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai'],
daysShort: ['Su', 'Ma', 'Ti', 'Ke', 'To', 'Pe', 'La'],
daysMin: ['Su', 'Ma', 'Ti', 'Ke', 'To', 'Pe', 'La'],
months: ['Tammikuu', 'Helmikuu', 'Maaliskuu', 'Huhtikuu', 'Toukokuu', 'Kesäkuu', 'Heinäkuu', 'Elokuu', 'Syyskuu', 'Lokakuu', 'Marraskuu', 'Joulukuu'],
monthsShort: ['Tammi', 'Helmi', 'Maalis', 'Huhti', 'Touko', 'Kesä', 'Heinä', 'Elo', 'Syys', 'Loka', 'Marras', 'Joulu'],
today: 'Tänään',
clear: 'Tyhjennä',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/fr' {
import {AirDatepickerLocale} from 'air-datepicker';
const fr: AirDatepickerLocale;
export default fr;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'],
daysShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
daysMin: ['Di', 'Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa'],
months: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
monthsShort: ['Jan', 'Fév', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Dec'],
today: "Aujourd'hui",
clear: 'Effacer',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/hu' {
import {AirDatepickerLocale} from 'air-datepicker';
const hu: AirDatepickerLocale;
export default hu;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Vasárnap', 'Hétfő', 'Kedd', 'Szerda', 'Csütörtök', 'Péntek', 'Szombat'],
daysShort: ['Va', 'Hé', 'Ke', 'Sze', 'Cs', 'Pé', 'Szo'],
daysMin: ['V', 'H', 'K', 'Sz', 'Cs', 'P', 'Sz'],
months: ['Január', 'Február', 'Március', 'Április', 'Május', 'Június', 'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Már', 'Ápr', 'Máj', 'Jún', 'Júl', 'Aug', 'Szep', 'Okt', 'Nov', 'Dec'],
today: 'Ma',
clear: 'Törlés',
dateFormat: 'yyyy-MM-dd',
timeFormat: 'hh:mm aa',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/it' {
import {AirDatepickerLocale} from 'air-datepicker';
const it: AirDatepickerLocale;
export default it;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
daysShort: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
daysMin: ['Do', 'Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa'],
months: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
monthsShort: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
today: 'Oggi',
clear: 'Cancella',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/ja' {
import {AirDatepickerLocale} from 'air-datepicker';
const ja: AirDatepickerLocale;
export default ja;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'],
daysShort: ['日', '月', '火', '水', '木', '金', '土'],
daysMin: ['日', '月', '火', '水', '木', '金', '土'],
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
today: '今日',
clear: 'クリア',
dateFormat: 'yyyy/MM/dd',
timeFormat: 'HH:mm',
firstDay: 0
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/ko' {
import {AirDatepickerLocale} from 'air-datepicker';
const ko: AirDatepickerLocale;
export default ko;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
daysShort: ['일', '월', '화', '수', '목', '금', '토'],
daysMin: ['일', '월', '화', '수', '목', '금', '토'],
months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
monthsShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
today: '오늘',
clear: '초기화',
dateFormat: 'MM/dd/yyyy',
timeFormat: 'hh:mm aa',
firstDay: 0
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/nl' {
import {AirDatepickerLocale} from 'air-datepicker';
const nl: AirDatepickerLocale;
export default nl;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
daysShort: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
daysMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
months: ['Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
today: 'Vandaag',
clear: 'Legen',
dateFormat: 'dd-MM-yyyy',
timeFormat: 'HH:mm',
firstDay: 0
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/pl' {
import {AirDatepickerLocale} from 'air-datepicker';
const pl: AirDatepickerLocale;
export default pl;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'],
daysShort: ['Nie', 'Pon', 'Wto', 'Śro', 'Czw', 'Pią', 'Sob'],
daysMin: ['Nd', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So'],
months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
monthsShort: ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'],
today: 'Dzisiaj',
clear: 'Wyczyść',
dateFormat: 'yyyy-MM-dd',
timeFormat: 'hh:mm:aa',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/pt-BR' {
import {AirDatepickerLocale} from 'air-datepicker';
const pt-BR: AirDatepickerLocale;
export default pt-BR;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'],
daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab'],
daysMin: ['Do', 'Se', 'Te', 'Qu', 'Qu', 'Se', 'Sa'],
months: ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'],
monthsShort: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
today: 'Hoje',
clear: 'Limpar',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'HH:mm',
firstDay: 0
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/pt' {
import {AirDatepickerLocale} from 'air-datepicker';
const pt: AirDatepickerLocale;
export default pt;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'],
daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab'],
daysMin: ['Do', 'Se', 'Te', 'Qa', 'Qi', 'Sx', 'Sa'],
months: ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'],
monthsShort: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
today: 'Hoje',
clear: 'Limpar',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/ro' {
import {AirDatepickerLocale} from 'air-datepicker';
const ro: AirDatepickerLocale;
export default ro;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Duminică', 'Luni', 'Marţi', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'],
daysShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'],
daysMin: ['D', 'L', 'Ma', 'Mi', 'J', 'V', 'S'],
months: ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'],
monthsShort: ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'],
today: 'Azi',
clear: 'Şterge',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/ru' {
import {AirDatepickerLocale} from 'air-datepicker';
const ru: AirDatepickerLocale;
export default ru;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'],
daysShort: ['Вос', 'Пон', 'Вто', 'Сре', 'Чет', 'Пят', 'Суб'],
daysMin: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'],
months: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
monthsShort: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
today: 'Сегодня',
clear: 'Очистить',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/si' {
import {AirDatepickerLocale} from 'air-datepicker';
const si: AirDatepickerLocale;
export default si;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['ඉරිදා', 'සදුදා', 'අඟහරැවදා', 'බදාදා', 'බ්‍රහස්‍පතින්‍', 'සිකුරාදා', 'සෙනසුරාදා'],
daysShort: ['ඉරිදා', 'සදුදා', 'අඟහ', 'බදාදා', 'බ්‍රහස්‍', 'සිකුරා', 'සෙන'],
daysMin: ['ඉරි', 'සදු', 'අඟ', 'බදා', 'බ්‍රහ', 'සිකු', 'සෙ'],
months: ['ජනවාරි', 'පෙබරවාරි', 'මාර්තු', 'අප්‍රේල්', 'මැයි', 'ජූනි', 'ජූලි', 'අගෝස්තු', 'සැප්තැම්බර්', 'ඔක්තෝබර්', 'නොවැම්බර්', 'දෙසැම්බර්'],
monthsShort: ['ජන', 'පෙබ', 'මාර්', 'අප්‍රේල්', 'මැයි', 'ජූනි', 'ජූලි', 'අගෝ', 'සැප්', 'ඔක්', 'නොවැ', 'දෙසැ'],
today: 'අද',
clear: 'යලි සකසන්න',
dateFormat: 'yyyy-mm-dd',
timeFormat: 'hh:ii aa',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/sk' {
import {AirDatepickerLocale} from 'air-datepicker';
const sk: AirDatepickerLocale;
export default sk;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Nedeľa', 'Pondelok', 'Utorok', 'Streda', 'Štvrtok', 'Piatok', 'Sobota'],
daysShort: ['Ned', 'Pon', 'Uto', 'Str', 'Štv', 'Pia', 'Sob'],
daysMin: ['Ne', 'Po', 'Ut', 'St', 'Št', 'Pi', 'So'],
months: ['Január', 'Február', 'Marec', 'Apríl', 'Máj', 'Jún', 'Júl', 'August', 'September', 'Október', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Máj', 'Jún', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
today: 'Dnes',
clear: 'Vymazať',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/sv' {
import {AirDatepickerLocale} from 'air-datepicker';
const sv: AirDatepickerLocale;
export default sv;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Söndag', 'Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag'],
daysShort: ['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör'],
daysMin: ['Sö', 'Må', 'Ti', 'On', 'To', 'Fr', 'Lö'],
months: ['Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
today: 'I dag',
clear: 'Nollställ',
dateFormat: 'yyyy-MM-dd',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/th' {
import {AirDatepickerLocale} from 'air-datepicker';
const th: AirDatepickerLocale;
export default th;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['วันอาทิตย์', 'วันจันทร์', 'วันอังคาร', 'วันพุธ', 'วันพฤหัสบดี', 'วันศุกร์', 'วันเสาร์'],
daysShort: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'],
daysMin: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'],
months: ['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'],
monthsShort: ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'],
today: 'วันนี้',
clear: 'ล้าง',
dateFormat: 'dd/MM/yyyy',
timeFormat: 'HH:mm',
firstDay: 0
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/tr' {
import {AirDatepickerLocale} from 'air-datepicker';
const tr: AirDatepickerLocale;
export default tr;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Pazar', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi'],
daysShort: ['Pzr', 'Pts', 'Sl', 'Çar', 'Per', 'Cum', 'Cts'],
daysMin: ['Pa', 'Pt', 'Sl', 'Ça', 'Pe', 'Cu', 'Ct'],
months: ['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'],
monthsShort: ['Oca', 'Şbt', 'Mrt', 'Nsn', 'Mys', 'Hzr', 'Tmz', 'Ağt', 'Eyl', 'Ekm', 'Ksm', 'Arl'],
today: 'Bugün',
clear: 'Temizle',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'hh:mm aa',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/uk' {
import {AirDatepickerLocale} from 'air-datepicker';
const uk: AirDatepickerLocale;
export default uk;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['Неділя', 'Понеділок', 'Вівторок', 'Середа', 'Четвер', 'П’ятниця', 'Субота'],
daysShort: ['Нед', 'Пнд', 'Вів', 'Срд', 'Чтв', 'Птн', 'Сбт'],
daysMin: ['Нд', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'],
months: ['Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень'],
monthsShort: ['Січ', 'Лют', 'Бер', 'Кві', 'Тра', 'Чер', 'Лип', 'Сер', 'Вер', 'Жов', 'Лис', 'Гру'],
today: 'Сьогодні',
clear: 'Очистити',
dateFormat: 'dd.MM.yyyy',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -1,6 +0,0 @@
declare module 'air-datepicker/locale/zh' {
import {AirDatepickerLocale} from 'air-datepicker';
const zh: AirDatepickerLocale;
export default zh;
}

View File

@ -1,19 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
days: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
daysShort: ['日', '一', '二', '三', '四', '五', '六'],
daysMin: ['日', '一', '二', '三', '四', '五', '六'],
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
monthsShort: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
today: '今天',
clear: '清除',
dateFormat: 'yyyy-MM-dd',
timeFormat: 'HH:mm',
firstDay: 1
};
exports.default = _default;

View File

@ -44,6 +44,7 @@ class darkMode {
this.htmlEl = document.querySelector("html");
this.darkToggleEl = document.querySelector("[dark-toggle]");
this.darkToggleLabel = document.querySelector("[dark-toggle-label]");
this.csrf = document.querySelector("input#csrf_token");
this.init();
}
@ -63,14 +64,13 @@ class darkMode {
async saveMode() {
const isDark = this.darkToggleEl.checked ? "true" : "false";
console.log(isDark);
const data = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ darkmode: isDark }),
body: JSON.stringify({ darkmode: isDark, csrf_token: this.csrf.value }),
};
const send = await fetch(`${location.href}/darkmode}`, data);
}

View File

@ -0,0 +1,210 @@
import {registerListeners, unregisterListeners} from './lib/event.js';
import {formatDate} from './lib/date-format.js';
import Datepicker from './Datepicker.js';
// filter out the config options inapproprite to pass to Datepicker
function filterOptions(options) {
const newOpts = Object.assign({}, options);
delete newOpts.inputs;
delete newOpts.allowOneSidedRange;
delete newOpts.maxNumberOfDates; // to ensure each datepicker handles a single date
return newOpts;
}
function setupDatepicker(rangepicker, changeDateListener, el, options) {
registerListeners(rangepicker, [
[el, 'changeDate', changeDateListener],
]);
new Datepicker(el, options, rangepicker);
}
function onChangeDate(rangepicker, ev) {
// to prevent both datepickers trigger the other side's update each other
if (rangepicker._updating) {
return;
}
rangepicker._updating = true;
const target = ev.target;
if (target.datepicker === undefined) {
return;
}
const datepickers = rangepicker.datepickers;
const setDateOptions = {render: false};
const changedSide = rangepicker.inputs.indexOf(target);
const otherSide = changedSide === 0 ? 1 : 0;
const changedDate = datepickers[changedSide].dates[0];
const otherDate = datepickers[otherSide].dates[0];
if (changedDate !== undefined && otherDate !== undefined) {
// if the start of the range > the end, swap them
if (changedSide === 0 && changedDate > otherDate) {
datepickers[0].setDate(otherDate, setDateOptions);
datepickers[1].setDate(changedDate, setDateOptions);
} else if (changedSide === 1 && changedDate < otherDate) {
datepickers[0].setDate(changedDate, setDateOptions);
datepickers[1].setDate(otherDate, setDateOptions);
}
} else if (!rangepicker.allowOneSidedRange) {
// to prevent the range from becoming one-sided, copy changed side's
// selection (no matter if it's empty) to the other side
if (changedDate !== undefined || otherDate !== undefined) {
setDateOptions.clear = true;
datepickers[otherSide].setDate(datepickers[changedSide].dates, setDateOptions);
}
}
datepickers[0].picker.update().render();
datepickers[1].picker.update().render();
delete rangepicker._updating;
}
/**
* Class representing a date range picker
*/
export default class DateRangePicker {
/**
* Create a date range picker
* @param {Element} element - element to bind a date range picker
* @param {Object} [options] - config options
*/
constructor(element, options = {}) {
const inputs = Array.isArray(options.inputs)
? options.inputs
: Array.from(element.querySelectorAll('input'));
if (inputs.length < 2) {
return;
}
element.rangepicker = this;
this.element = element;
this.inputs = inputs.slice(0, 2);
this.allowOneSidedRange = !!options.allowOneSidedRange;
const changeDateListener = onChangeDate.bind(null, this);
const cleanOptions = filterOptions(options);
// in order for initial date setup to work right when pcicLvel > 0,
// let Datepicker constructor add the instance to the rangepicker
const datepickers = [];
Object.defineProperty(this, 'datepickers', {
get() {
return datepickers;
},
});
setupDatepicker(this, changeDateListener, this.inputs[0], cleanOptions);
setupDatepicker(this, changeDateListener, this.inputs[1], cleanOptions);
Object.freeze(datepickers);
// normalize the range if inital dates are given
if (datepickers[0].dates.length > 0) {
onChangeDate(this, {target: this.inputs[0]});
} else if (datepickers[1].dates.length > 0) {
onChangeDate(this, {target: this.inputs[1]});
}
}
/**
* @type {Array} - selected date of the linked date pickers
*/
get dates() {
return this.datepickers.length === 2
? [
this.datepickers[0].dates[0],
this.datepickers[1].dates[0],
]
: undefined;
}
/**
* Set new values to the config options
* @param {Object} options - config options to update
*/
setOptions(options) {
this.allowOneSidedRange = !!options.allowOneSidedRange;
const cleanOptions = filterOptions(options);
this.datepickers[0].setOptions(cleanOptions);
this.datepickers[1].setOptions(cleanOptions);
}
/**
* Destroy the DateRangePicker instance
* @return {DateRangePicker} - the instance destroyed
*/
destroy() {
this.datepickers[0].destroy();
this.datepickers[1].destroy();
unregisterListeners(this);
delete this.element.rangepicker;
}
/**
* Get the start and end dates of the date range
*
* The method returns Date objects by default. If format string is passed,
* it returns date strings formatted in given format.
* The result array always contains 2 items (start date/end date) and
* undefined is used for unselected side. (e.g. If none is selected,
* the result will be [undefined, undefined]. If only the end date is set
* when allowOneSidedRange config option is true, [undefined, endDate] will
* be returned.)
*
* @param {String} [format] - Format string to stringify the dates
* @return {Array} - Start and end dates
*/
getDates(format = undefined) {
const callback = format
? date => formatDate(date, format, this.datepickers[0].config.locale)
: date => new Date(date);
return this.dates.map(date => date === undefined ? date : callback(date));
}
/**
* Set the start and end dates of the date range
*
* The method calls datepicker.setDate() internally using each of the
* arguments in startend order.
*
* When a clear: true option object is passed instead of a date, the method
* clears the date.
*
* If an invalid date, the same date as the current one or an option object
* without clear: true is passed, the method considers that argument as an
* "ineffective" argument because calling datepicker.setDate() with those
* values makes no changes to the date selection.
*
* When the allowOneSidedRange config option is false, passing {clear: true}
* to clear the range works only when it is done to the last effective
* argument (in other words, passed to rangeEnd or to rangeStart along with
* ineffective rangeEnd). This is because when the date range is changed,
* it gets normalized based on the last change at the end of the changing
* process.
*
* @param {Date|Number|String|Object} rangeStart - Start date of the range
* or {clear: true} to clear the date
* @param {Date|Number|String|Object} rangeEnd - End date of the range
* or {clear: true} to clear the date
*/
setDates(rangeStart, rangeEnd) {
const [datepicker0, datepicker1] = this.datepickers;
const origDates = this.dates;
// If range normalization runs on every change, we can't set a new range
// that starts after the end of the current range correctly because the
// normalization process swaps start↔end right after setting the new start
// date. To prevent this, the normalization process needs to run once after
// both of the new dates are set.
this._updating = true;
datepicker0.setDate(rangeStart);
datepicker1.setDate(rangeEnd);
delete this._updating;
if (datepicker1.dates[0] !== origDates[1]) {
onChangeDate(this, {target: this.inputs[1]});
} else if (datepicker0.dates[0] !== origDates[0]) {
onChangeDate(this, {target: this.inputs[0]});
}
}
}

View File

@ -0,0 +1,502 @@
import {lastItemOf, stringToArray, isInRange} from './lib/utils.js';
import {today, regularizeDate} from './lib/date.js';
import {parseDate, formatDate} from './lib/date-format.js';
import {isActiveElement} from './lib/dom.js';
import {registerListeners, unregisterListeners} from './lib/event.js';
import {locales} from './i18n/base-locales.js';
import defaultOptions from './options/defaultOptions.js';
import processOptions from './options/processOptions.js';
import Picker from './picker/Picker.js';
import {triggerDatepickerEvent} from './events/functions.js';
import {onKeydown, onFocus, onMousedown, onClickInput, onPaste} from './events/inputFieldListeners.js';
import {onClickOutside} from './events/otherListeners.js';
function stringifyDates(dates, config) {
return dates
.map(dt => formatDate(dt, config.format, config.locale))
.join(config.dateDelimiter);
}
// parse input dates and create an array of time values for selection
// returns undefined if there are no valid dates in inputDates
// when origDates (current selection) is passed, the function works to mix
// the input dates into the current selection
function processInputDates(datepicker, inputDates, clear = false) {
// const {config, dates: origDates, rangepicker} = datepicker;
const {config, dates: origDates, rangeSideIndex} = datepicker;
if (inputDates.length === 0) {
// empty input is considered valid unless origiDates is passed
return clear ? [] : undefined;
}
// const rangeEnd = rangepicker && datepicker === rangepicker.datepickers[1];
let newDates = inputDates.reduce((dates, dt) => {
let date = parseDate(dt, config.format, config.locale);
if (date === undefined) {
return dates;
}
// adjust to 1st of the month/Jan 1st of the year
// or to the last day of the monh/Dec 31st of the year if the datepicker
// is the range-end picker of a rangepicker
date = regularizeDate(date, config.pickLevel, rangeSideIndex);
if (
isInRange(date, config.minDate, config.maxDate)
&& !dates.includes(date)
&& !config.datesDisabled.includes(date)
&& (config.pickLevel > 0 || !config.daysOfWeekDisabled.includes(new Date(date).getDay()))
) {
dates.push(date);
}
return dates;
}, []);
if (newDates.length === 0) {
return;
}
if (config.multidate && !clear) {
// get the synmetric difference between origDates and newDates
newDates = newDates.reduce((dates, date) => {
if (!origDates.includes(date)) {
dates.push(date);
}
return dates;
}, origDates.filter(date => !newDates.includes(date)));
}
// do length check always because user can input multiple dates regardless of the mode
return config.maxNumberOfDates && newDates.length > config.maxNumberOfDates
? newDates.slice(config.maxNumberOfDates * -1)
: newDates;
}
// refresh the UI elements
// modes: 1: input only, 2, picker only, 3 both
function refreshUI(datepicker, mode = 3, quickRender = true) {
const {config, picker, inputField} = datepicker;
if (mode & 2) {
const newView = picker.active ? config.pickLevel : config.startView;
picker.update().changeView(newView).render(quickRender);
}
if (mode & 1 && inputField) {
inputField.value = stringifyDates(datepicker.dates, config);
}
}
function setDate(datepicker, inputDates, options) {
let {clear, render, autohide, revert} = options;
if (render === undefined) {
render = true;
}
if (!render) {
autohide = false;
} else if (autohide === undefined) {
autohide = datepicker.config.autohide;
}
const newDates = processInputDates(datepicker, inputDates, clear);
if (!newDates && !revert) {
return;
}
if (newDates && newDates.toString() !== datepicker.dates.toString()) {
datepicker.dates = newDates;
refreshUI(datepicker, render ? 3 : 1);
triggerDatepickerEvent(datepicker, 'changeDate');
} else {
refreshUI(datepicker, 1);
}
if (autohide) {
datepicker.hide();
}
}
/**
* Class representing a date picker
*/
export default class Datepicker {
/**
* Create a date picker
* @param {Element} element - element to bind a date picker
* @param {Object} [options] - config options
* @param {DateRangePicker} [rangepicker] - DateRangePicker instance the
* date picker belongs to. Use this only when creating date picker as a part
* of date range picker
*/
constructor(element, options = {}, rangepicker = undefined) {
element.datepicker = this;
this.element = element;
const config = this.config = Object.assign({
buttonClass: (options.buttonClass && String(options.buttonClass)) || 'button',
container: null,
defaultViewDate: today(),
maxDate: undefined,
minDate: undefined,
}, processOptions(defaultOptions, this));
// configure by type
const inline = this.inline = element.tagName !== 'INPUT';
let inputField;
if (inline) {
config.container = element;
} else {
if (options.container) {
// omit string type check because it doesn't guarantee to avoid errors
// (invalid selector string causes abend with sytax error)
config.container = options.container instanceof HTMLElement
? options.container
: document.querySelector(options.container);
}
inputField = this.inputField = element;
inputField.classList.add('datepicker-input');
}
if (rangepicker) {
// check validiry
const index = rangepicker.inputs.indexOf(inputField);
const datepickers = rangepicker.datepickers;
if (index < 0 || index > 1 || !Array.isArray(datepickers)) {
throw Error('Invalid rangepicker object.');
}
// attach itaelf to the rangepicker here so that processInputDates() can
// determine if this is the range-end picker of the rangepicker while
// setting inital values when pickLevel > 0
datepickers[index] = this;
// add getter for rangepicker
Object.defineProperty(this, 'rangepicker', {
get() {
return rangepicker;
},
});
Object.defineProperty(this, 'rangeSideIndex', {
get() {
return index;
},
});
}
// set up config
this._options = options;
Object.assign(config, processOptions(options, this));
// set initial dates
let initialDates;
if (inline) {
initialDates = stringToArray(element.dataset.date, config.dateDelimiter);
delete element.dataset.date;
} else {
initialDates = stringToArray(inputField.value, config.dateDelimiter);
}
this.dates = [];
// process initial value
const inputDateValues = processInputDates(this, initialDates);
if (inputDateValues && inputDateValues.length > 0) {
this.dates = inputDateValues;
}
if (inputField) {
inputField.value = stringifyDates(this.dates, config);
}
const picker = this.picker = new Picker(this);
if (inline) {
this.show();
} else {
// set up event listeners in other modes
const onMousedownDocument = onClickOutside.bind(null, this);
const listeners = [
[inputField, 'keydown', onKeydown.bind(null, this)],
[inputField, 'focus', onFocus.bind(null, this)],
[inputField, 'mousedown', onMousedown.bind(null, this)],
[inputField, 'click', onClickInput.bind(null, this)],
[inputField, 'paste', onPaste.bind(null, this)],
[document, 'mousedown', onMousedownDocument],
[document, 'touchstart', onMousedownDocument],
[window, 'resize', picker.place.bind(picker)]
];
registerListeners(this, listeners);
}
}
/**
* Format Date object or time value in given format and language
* @param {Date|Number} date - date or time value to format
* @param {String|Object} format - format string or object that contains
* toDisplay() custom formatter, whose signature is
* - args:
* - date: {Date} - Date instance of the date passed to the method
* - format: {Object} - the format object passed to the method
* - locale: {Object} - locale for the language specified by `lang`
* - return:
* {String} formatted date
* @param {String} [lang=en] - language code for the locale to use
* @return {String} formatted date
*/
static formatDate(date, format, lang) {
return formatDate(date, format, lang && locales[lang] || locales.en);
}
/**
* Parse date string
* @param {String|Date|Number} dateStr - date string, Date object or time
* value to parse
* @param {String|Object} format - format string or object that contains
* toValue() custom parser, whose signature is
* - args:
* - dateStr: {String|Date|Number} - the dateStr passed to the method
* - format: {Object} - the format object passed to the method
* - locale: {Object} - locale for the language specified by `lang`
* - return:
* {Date|Number} parsed date or its time value
* @param {String} [lang=en] - language code for the locale to use
* @return {Number} time value of parsed date
*/
static parseDate(dateStr, format, lang) {
return parseDate(dateStr, format, lang && locales[lang] || locales.en);
}
/**
* @type {Object} - Installed locales in `[languageCode]: localeObject` format
* en`:_English (US)_ is pre-installed.
*/
static get locales() {
return locales;
}
/**
* @type {Boolean} - Whether the picker element is shown. `true` whne shown
*/
get active() {
return !!(this.picker && this.picker.active);
}
/**
* @type {HTMLDivElement} - DOM object of picker element
*/
get pickerElement() {
return this.picker ? this.picker.element : undefined;
}
/**
* Set new values to the config options
* @param {Object} options - config options to update
*/
setOptions(options) {
const picker = this.picker;
const newOptions = processOptions(options, this);
Object.assign(this._options, options);
Object.assign(this.config, newOptions);
picker.setOptions(newOptions);
refreshUI(this, 3);
}
/**
* Show the picker element
*/
show() {
if (this.inputField) {
if (this.inputField.disabled) {
return;
}
if (!isActiveElement(this.inputField) && !this.config.disableTouchKeyboard) {
this._showing = true;
this.inputField.focus();
delete this._showing;
}
}
this.picker.show();
}
/**
* Hide the picker element
* Not available on inline picker
*/
hide() {
if (this.inline) {
return;
}
this.picker.hide();
this.picker.update().changeView(this.config.startView).render();
}
/**
* Destroy the Datepicker instance
* @return {Detepicker} - the instance destroyed
*/
destroy() {
this.hide();
unregisterListeners(this);
this.picker.detach();
if (!this.inline) {
this.inputField.classList.remove('datepicker-input');
}
delete this.element.datepicker;
return this;
}
/**
* Get the selected date(s)
*
* The method returns a Date object of selected date by default, and returns
* an array of selected dates in multidate mode. If format string is passed,
* it returns date string(s) formatted in given format.
*
* @param {String} [format] - Format string to stringify the date(s)
* @return {Date|String|Date[]|String[]} - selected date(s), or if none is
* selected, empty array in multidate mode and untitled in sigledate mode
*/
getDate(format = undefined) {
const callback = format
? date => formatDate(date, format, this.config.locale)
: date => new Date(date);
if (this.config.multidate) {
return this.dates.map(callback);
}
if (this.dates.length > 0) {
return callback(this.dates[0]);
}
}
/**
* Set selected date(s)
*
* In multidate mode, you can pass multiple dates as a series of arguments
* or an array. (Since each date is parsed individually, the type of the
* dates doesn't have to be the same.)
* The given dates are used to toggle the select status of each date. The
* number of selected dates is kept from exceeding the length set to
* maxNumberOfDates.
*
* With clear: true option, the method can be used to clear the selection
* and to replace the selection instead of toggling in multidate mode.
* If the option is passed with no date arguments or an empty dates array,
* it works as "clear" (clear the selection then set nothing), and if the
* option is passed with new dates to select, it works as "replace" (clear
* the selection then set the given dates)
*
* When render: false option is used, the method omits re-rendering the
* picker element. In this case, you need to call refresh() method later in
* order for the picker element to reflect the changes. The input field is
* refreshed always regardless of this option.
*
* When invalid (unparsable, repeated, disabled or out-of-range) dates are
* passed, the method ignores them and applies only valid ones. In the case
* that all the given dates are invalid, which is distinguished from passing
* no dates, the method considers it as an error and leaves the selection
* untouched. (The input field also remains untouched unless revert: true
* option is used.)
*
* @param {...(Date|Number|String)|Array} [dates] - Date strings, Date
* objects, time values or mix of those for new selection
* @param {Object} [options] - function options
* - clear: {boolean} - Whether to clear the existing selection
* defualt: false
* - render: {boolean} - Whether to re-render the picker element
* default: true
* - autohide: {boolean} - Whether to hide the picker element after re-render
* Ignored when used with render: false
* default: config.autohide
* - revert: {boolean} - Whether to refresh the input field when all the
* passed dates are invalid
* default: false
*/
setDate(...args) {
const dates = [...args];
const opts = {};
const lastArg = lastItemOf(args);
if (
typeof lastArg === 'object'
&& !Array.isArray(lastArg)
&& !(lastArg instanceof Date)
&& lastArg
) {
Object.assign(opts, dates.pop());
}
const inputDates = Array.isArray(dates[0]) ? dates[0] : dates;
setDate(this, inputDates, opts);
}
/**
* Update the selected date(s) with input field's value
* Not available on inline picker
*
* The input field will be refreshed with properly formatted date string.
*
* In the case that all the entered dates are invalid (unparsable, repeated,
* disabled or out-of-range), whixh is distinguished from empty input field,
* the method leaves the input field untouched as well as the selection by
* default. If revert: true option is used in this case, the input field is
* refreshed with the existing selection.
*
* @param {Object} [options] - function options
* - autohide: {boolean} - whether to hide the picker element after refresh
* default: false
* - revert: {boolean} - Whether to refresh the input field when all the
* passed dates are invalid
* default: false
*/
update(options = undefined) {
if (this.inline) {
return;
}
const opts = Object.assign(options || {}, {clear: true, render: true});
const inputDates = stringToArray(this.inputField.value, this.config.dateDelimiter);
setDate(this, inputDates, opts);
}
/**
* Refresh the picker element and the associated input field
* @param {String} [target] - target item when refreshing one item only
* 'picker' or 'input'
* @param {Boolean} [forceRender] - whether to re-render the picker element
* regardless of its state instead of optimized refresh
*/
refresh(target = undefined, forceRender = false) {
if (target && typeof target !== 'string') {
forceRender = target;
target = undefined;
}
let mode;
if (target === 'picker') {
mode = 2;
} else if (target === 'input') {
mode = 1;
} else {
mode = 3;
}
refreshUI(this, mode, !forceRender);
}
/**
* Enter edit mode
* Not available on inline picker or when the picker element is hidden
*/
enterEditMode() {
if (this.inline || !this.picker.active || this.editMode) {
return;
}
this.editMode = true;
this.inputField.classList.add('in-edit');
}
/**
* Exit from edit mode
* Not available on inline picker
* @param {Object} [options] - function options
* - update: {boolean} - whether to call update() after exiting
* If false, input field is revert to the existing selection
* default: false
*/
exitEditMode(options = undefined) {
if (this.inline || !this.editMode) {
return;
}
const opts = Object.assign({update: false}, options);
delete this.editMode;
this.inputField.classList.remove('in-edit');
if (opts.update) {
this.update(opts);
}
}
}

View File

@ -0,0 +1,5 @@
import Datepicker from './Datepicker.js';
import DateRangePicker from './DateRangePicker.js';
window.Datepicker = Datepicker;
window.DateRangePicker = DateRangePicker;

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,48 @@
import {limitToRange} from '../lib/utils.js';
import {addMonths, addYears} from '../lib/date.js';
export function triggerDatepickerEvent(datepicker, type) {
const detail = {
date: datepicker.getDate(),
viewDate: new Date(datepicker.picker.viewDate),
viewId: datepicker.picker.currentView.id,
datepicker,
};
datepicker.element.dispatchEvent(new CustomEvent(type, {detail}));
}
// direction: -1 (to previous), 1 (to next)
export function goToPrevOrNext(datepicker, direction) {
const {minDate, maxDate} = datepicker.config;
const {currentView, viewDate} = datepicker.picker;
let newViewDate;
switch (currentView.id) {
case 0:
newViewDate = addMonths(viewDate, direction);
break;
case 1:
newViewDate = addYears(viewDate, direction);
break;
default:
newViewDate = addYears(viewDate, direction * currentView.navStep);
}
newViewDate = limitToRange(newViewDate, minDate, maxDate);
datepicker.picker.changeFocus(newViewDate).render();
}
export function switchView(datepicker) {
const viewId = datepicker.picker.currentView.id;
if (viewId === datepicker.config.maxView) {
return;
}
datepicker.picker.changeView(viewId + 1).render();
}
export function unfocus(datepicker) {
if (datepicker.config.updateOnBlur) {
datepicker.update({revert: true});
} else {
datepicker.refresh('input');
}
datepicker.hide();
}

View File

@ -0,0 +1,195 @@
import {isInRange} from '../lib/utils.js';
import {isActiveElement} from '../lib/dom.js';
import {addDays, addMonths, addYears, startOfYearPeriod} from '../lib/date.js';
import {goToPrevOrNext, switchView, unfocus} from './functions.js';
// Find the closest date that doesn't meet the condition for unavailable date
// Returns undefined if no available date is found
// addFn: function to calculate the next date
// - args: time value, amount
// increase: amount to pass to addFn
// testFn: function to test the unavailablity of the date
// - args: time value; retun: true if unavailable
function findNextAvailableOne(date, addFn, increase, testFn, min, max) {
if (!isInRange(date, min, max)) {
return;
}
if (testFn(date)) {
const newDate = addFn(date, increase);
return findNextAvailableOne(newDate, addFn, increase, testFn, min, max);
}
return date;
}
// direction: -1 (left/up), 1 (right/down)
// vertical: true for up/down, false for left/right
function moveByArrowKey(datepicker, ev, direction, vertical) {
const picker = datepicker.picker;
const currentView = picker.currentView;
const step = currentView.step || 1;
let viewDate = picker.viewDate;
let addFn;
let testFn;
switch (currentView.id) {
case 0:
if (vertical) {
viewDate = addDays(viewDate, direction * 7);
} else if (ev.ctrlKey || ev.metaKey) {
viewDate = addYears(viewDate, direction);
} else {
viewDate = addDays(viewDate, direction);
}
addFn = addDays;
testFn = (date) => currentView.disabled.includes(date);
break;
case 1:
viewDate = addMonths(viewDate, vertical ? direction * 4 : direction);
addFn = addMonths;
testFn = (date) => {
const dt = new Date(date);
const {year, disabled} = currentView;
return dt.getFullYear() === year && disabled.includes(dt.getMonth());
};
break;
default:
viewDate = addYears(viewDate, direction * (vertical ? 4 : 1) * step);
addFn = addYears;
testFn = date => currentView.disabled.includes(startOfYearPeriod(date, step));
}
viewDate = findNextAvailableOne(
viewDate,
addFn,
direction < 0 ? -step : step,
testFn,
currentView.minDate,
currentView.maxDate
);
if (viewDate !== undefined) {
picker.changeFocus(viewDate).render();
}
}
export function onKeydown(datepicker, ev) {
const key = ev.key;
if (key === 'Tab') {
unfocus(datepicker);
return;
}
const picker = datepicker.picker;
const {id, isMinView} = picker.currentView;
if (!picker.active) {
if (key === 'ArrowDown') {
picker.show();
} else {
if (key === 'Enter') {
datepicker.update();
} else if (key === 'Escape') {
picker.show();
}
return;
}
} else if (datepicker.editMode) {
if (key === 'Enter') {
datepicker.exitEditMode({update: true, autohide: datepicker.config.autohide});
} else if (key === 'Escape') {
picker.hide();
}
return;
} else {
if (key === 'ArrowLeft') {
if (ev.ctrlKey || ev.metaKey) {
goToPrevOrNext(datepicker, -1);
} else if (ev.shiftKey) {
datepicker.enterEditMode();
return;
} else {
moveByArrowKey(datepicker, ev, -1, false);
}
} else if (key === 'ArrowRight') {
if (ev.ctrlKey || ev.metaKey) {
goToPrevOrNext(datepicker, 1);
} else if (ev.shiftKey) {
datepicker.enterEditMode();
return;
} else {
moveByArrowKey(datepicker, ev, 1, false);
}
} else if (key === 'ArrowUp') {
if (ev.ctrlKey || ev.metaKey) {
switchView(datepicker);
} else if (ev.shiftKey) {
datepicker.enterEditMode();
return;
} else {
moveByArrowKey(datepicker, ev, -1, true);
}
} else if (key === 'ArrowDown') {
if (ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
datepicker.enterEditMode();
return;
}
moveByArrowKey(datepicker, ev, 1, true);
} else if (key === 'Enter') {
if (isMinView) {
datepicker.setDate(picker.viewDate);
return;
}
picker.changeView(id - 1).render();
} else {
if (key === 'Escape') {
picker.hide();
} else if (
key === 'Backspace'
|| key === 'Delete'
|| (key.length === 1 && !ev.ctrlKey && !ev.metaKey)
) {
datepicker.enterEditMode();
}
return;
}
}
ev.preventDefault();
}
export function onFocus(datepicker) {
if (datepicker.config.showOnFocus && !datepicker._showing) {
datepicker.show();
}
}
// for the prevention for entering edit mode while getting focus on click
export function onMousedown(datepicker, ev) {
const el = ev.target;
if (datepicker.picker.active || datepicker.config.showOnClick) {
el._active = isActiveElement(el);
el._clicking = setTimeout(() => {
delete el._active;
delete el._clicking;
}, 2000);
}
}
export function onClickInput(datepicker, ev) {
const el = ev.target;
if (!el._clicking) {
return;
}
clearTimeout(el._clicking);
delete el._clicking;
if (el._active) {
datepicker.enterEditMode();
}
delete el._active;
if (datepicker.config.showOnClick) {
datepicker.show();
}
}
export function onPaste(datepicker, ev) {
if (ev.clipboardData.types.includes('text/plain')) {
datepicker.enterEditMode();
}
}

View File

@ -0,0 +1,20 @@
import {isActiveElement} from '../lib/dom.js';
import {findElementInEventPath} from '../lib/event.js';
import {unfocus} from './functions.js';
// for the `document` to delegate the events from outside the picker/input field
export function onClickOutside(datepicker, ev) {
const {element, picker} = datepicker;
// check both picker's and input's activeness to make updateOnBlur work in
// the cases where...
// - picker is hidden by ESC key press → input stays focused
// - input is unfocused by closing mobile keyboard → piker is kept shown
if (!picker.active && !isActiveElement(element)) {
return;
}
const pickerElem = picker.element;
if (findElementInEventPath(ev, el => el === element || el === pickerElem)) {
return;
}
unfocus(datepicker);
}

View File

@ -0,0 +1,68 @@
import {today, addMonths, addYears} from '../lib/date.js';
import {findElementInEventPath} from '../lib/event.js';
import {goToPrevOrNext, switchView} from './functions.js';
function goToSelectedMonthOrYear(datepicker, selection) {
const picker = datepicker.picker;
const viewDate = new Date(picker.viewDate);
const viewId = picker.currentView.id;
const newDate = viewId === 1
? addMonths(viewDate, selection - viewDate.getMonth())
: addYears(viewDate, selection - viewDate.getFullYear());
picker.changeFocus(newDate).changeView(viewId - 1).render();
}
export function onClickTodayBtn(datepicker) {
const picker = datepicker.picker;
const currentDate = today();
if (datepicker.config.todayBtnMode === 1) {
if (datepicker.config.autohide) {
datepicker.setDate(currentDate);
return;
}
datepicker.setDate(currentDate, {render: false});
picker.update();
}
if (picker.viewDate !== currentDate) {
picker.changeFocus(currentDate);
}
picker.changeView(0).render();
}
export function onClickClearBtn(datepicker) {
datepicker.setDate({clear: true});
}
export function onClickViewSwitch(datepicker) {
switchView(datepicker);
}
export function onClickPrevBtn(datepicker) {
goToPrevOrNext(datepicker, -1);
}
export function onClickNextBtn(datepicker) {
goToPrevOrNext(datepicker, 1);
}
// For the picker's main block to delegete the events from `datepicker-cell`s
export function onClickView(datepicker, ev) {
const target = findElementInEventPath(ev, '.datepicker-cell');
if (!target || target.classList.contains('disabled')) {
return;
}
const {id, isMinView} = datepicker.picker.currentView;
if (isMinView) {
datepicker.setDate(Number(target.dataset.date));
} else if (id === 1) {
goToSelectedMonthOrYear(datepicker, Number(target.dataset.month));
} else {
goToSelectedMonthOrYear(datepicker, Number(target.dataset.year));
}
}
export function onMousedownPicker(ev) {
ev.preventDefault();
}

View File

@ -0,0 +1,13 @@
// default locales
export const locales = {
en: {
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
today: "Today",
clear: "Clear",
titleFormat: "MM y"
}
};

View File

@ -0,0 +1,19 @@
/**
* Arabic-Algeria translation for bootstrap-datepicker
* Rabah Saadi <infosrabah@gmail.com>
*/
export default {
'ar-DZ': {
days: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
daysShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"],
daysMin: ["ح", "ن", "ث", "ع", "خ", "ج", "س", "ح"],
months: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
monthsShort: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
today: "هذا اليوم",
rtl: true,
monthsTitle: "أشهر",
clear: "إزالة",
format: "yyyy/mm/dd",
weekStart: 0
}
};

View File

@ -0,0 +1,15 @@
/**
* Arabic-Tunisia translation for bootstrap-datepicker
* Souhaieb Besbes <besbes.souhaieb@gmail.com>
*/
export default {
'ar-tn': {
days: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
daysShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"],
daysMin: ["ح", "ن", "ث", "ع", "خ", "ج", "س", "ح"],
months: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
monthsShort: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
today: "هذا اليوم",
rtl: true
}
};

View File

@ -0,0 +1,15 @@
/**
* Arabic translation for bootstrap-datepicker
* Mohammed Alshehri <alshehri866@gmail.com>
*/
export default {
ar: {
days: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
daysShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"],
daysMin: ["ح", "ن", "ث", "ع", "خ", "ج", "س", "ح"],
months: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
monthsShort: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
today: "هذا اليوم",
rtl: true
}
};

View File

@ -0,0 +1,14 @@
// Azerbaijani
export default {
az: {
days: ["Bazar", "Bazar ertəsi", "Çərşənbə axşamı", "Çərşənbə", "Cümə axşamı", "Cümə", "Şənbə"],
daysShort: ["B.", "B.e", "Ç.a", "Ç.", "C.a", "C.", "Ş."],
daysMin: ["B.", "B.e", "Ç.a", "Ç.", "C.a", "C.", "Ş."],
months: ["Yanvar", "Fevral", "Mart", "Aprel", "May", "İyun", "İyul", "Avqust", "Sentyabr", "Oktyabr", "Noyabr", "Dekabr"],
monthsShort: ["Yan", "Fev", "Mar", "Apr", "May", "İyun", "İyul", "Avq", "Sen", "Okt", "Noy", "Dek"],
today: "Bu gün",
weekStart: 1,
clear: "Təmizlə",
monthsTitle: 'Aylar'
}
};

View File

@ -0,0 +1,14 @@
/**
* Bulgarian translation for bootstrap-datepicker
* Apostol Apostolov <apostol.s.apostolov@gmail.com>
*/
export default {
bg: {
days: ["Неделя", "Понеделник", "Вторник", "Сряда", "Четвъртък", "Петък", "Събота"],
daysShort: ["Нед", "Пон", "Вто", "Сря", "Чет", "Пет", "Съб"],
daysMin: ["Н", "П", "В", "С", "Ч", "П", "С"],
months: ["Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември"],
monthsShort: ["Ян", "Фев", "Мар", "Апр", "Май", "Юни", "Юли", "Авг", "Сеп", "Окт", "Ное", "Дек"],
today: "днес"
}
};

View File

@ -0,0 +1,18 @@
/**
* Bamanankan (bm) translation for bootstrap-datepicker
* Fatou Fall <fatou@medicmobile.org>
*/
export default {
bm: {
days: ["Kari","Ntɛnɛn","Tarata","Araba","Alamisa","Juma","Sibiri"],
daysShort: ["Kar","Ntɛ","Tar","Ara","Ala","Jum","Sib"],
daysMin: ["Ka","Nt","Ta","Ar","Al","Ju","Si"],
months: ["Zanwuyekalo","Fewuruyekalo","Marisikalo","Awirilikalo","Mɛkalo","Zuwɛnkalo","Zuluyekalo","Utikalo","Sɛtanburukalo","ɔkutɔburukalo","Nowanburukalo","Desanburukalo"],
monthsShort: ["Zan","Few","Mar","Awi","Mɛ","Zuw","Zul","Uti","Sɛt","ɔku","Now","Des"],
today: "Bi",
monthsTitle: "Kalo",
clear: "Ka jɔsi",
weekStart: 1,
format: "dd/mm/yyyy"
}
};

Some files were not shown because too many files have changed in this diff Show More