Merge pull request #363 from TheophileDiot/dev
Advancements on the UI, jobs and database
This commit is contained in:
commit
a0634b5736
|
@ -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
|
||||
|
|
|
@ -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 && \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()}")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 && \
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 && \
|
||||
|
|
153
src/ui/main.py
153
src/ui/main.py
|
@ -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():
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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)}
|
|
@ -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);
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -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 */
|
|
@ -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 */
|
||||
|
|
|
@ -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
|
@ -1,2 +0,0 @@
|
|||
import AirDatepicker from "air-datepicker";
|
||||
export default AirDatepicker;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/ar' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const ar: AirDatepickerLocale;
|
||||
|
||||
export default ar;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/cs' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const cs: AirDatepickerLocale;
|
||||
|
||||
export default cs;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/da' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const da: AirDatepickerLocale;
|
||||
|
||||
export default da;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/de' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const de: AirDatepickerLocale;
|
||||
|
||||
export default de;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/en' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const en: AirDatepickerLocale;
|
||||
|
||||
export default en;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/es' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const es: AirDatepickerLocale;
|
||||
|
||||
export default es;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/fi' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const fi: AirDatepickerLocale;
|
||||
|
||||
export default fi;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/fr' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const fr: AirDatepickerLocale;
|
||||
|
||||
export default fr;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/hu' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const hu: AirDatepickerLocale;
|
||||
|
||||
export default hu;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/it' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const it: AirDatepickerLocale;
|
||||
|
||||
export default it;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/ja' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const ja: AirDatepickerLocale;
|
||||
|
||||
export default ja;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/ko' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const ko: AirDatepickerLocale;
|
||||
|
||||
export default ko;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/nl' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const nl: AirDatepickerLocale;
|
||||
|
||||
export default nl;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/pl' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const pl: AirDatepickerLocale;
|
||||
|
||||
export default pl;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/pt-BR' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const pt-BR: AirDatepickerLocale;
|
||||
|
||||
export default pt-BR;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/pt' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const pt: AirDatepickerLocale;
|
||||
|
||||
export default pt;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/ro' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const ro: AirDatepickerLocale;
|
||||
|
||||
export default ro;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/ru' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const ru: AirDatepickerLocale;
|
||||
|
||||
export default ru;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/si' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const si: AirDatepickerLocale;
|
||||
|
||||
export default si;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/sk' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const sk: AirDatepickerLocale;
|
||||
|
||||
export default sk;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/sv' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const sv: AirDatepickerLocale;
|
||||
|
||||
export default sv;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/th' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const th: AirDatepickerLocale;
|
||||
|
||||
export default th;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/tr' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const tr: AirDatepickerLocale;
|
||||
|
||||
export default tr;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/uk' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const uk: AirDatepickerLocale;
|
||||
|
||||
export default uk;
|
||||
}
|
|
@ -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;
|
|
@ -1,6 +0,0 @@
|
|||
declare module 'air-datepicker/locale/zh' {
|
||||
import {AirDatepickerLocale} from 'air-datepicker';
|
||||
const zh: AirDatepickerLocale;
|
||||
|
||||
export default zh;
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 start→end 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]});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
};
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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'
|
||||
}
|
||||
};
|
|
@ -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: "днес"
|
||||
}
|
||||
};
|
|
@ -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
Loading…
Reference in New Issue