2825 lines
77 KiB
JavaScript
2825 lines
77 KiB
JavaScript
// ==UserScript==
|
||
// @name Runcity
|
||
// @namespace http://tampermonkey.net/
|
||
// @version 2024-11-18
|
||
// @description Prettify pages & store data on server
|
||
// @author You
|
||
// @match https://rst.runcity.org/*
|
||
// @icon https://www.google.com/s2/favicons?sz=64&domain=runcity.org
|
||
// @grant none
|
||
// @require https://code.jquery.com/jquery-3.7.1.min.js
|
||
// @require https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js
|
||
// @require https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js
|
||
// ==/UserScript==
|
||
|
||
(function () {
|
||
'use strict'
|
||
|
||
const adminSite = "runcity.geo.rictum.ru"
|
||
|
||
const links = {
|
||
"Волонтеры": "suv_comp",
|
||
"Контрольные пункты": "cp_mgmt",
|
||
"+ КП": "cp_mgmt/?action=edit",
|
||
"Маршруты": "route_mgmt",
|
||
"Тесты": "route_mgmt?test=1",
|
||
"+ Тест": "suv_comp?action=test_online",
|
||
"Склеить": "suv_comp?action=test_merge",
|
||
"Прохождение": "callcenter?action=online_log",
|
||
"КП в трассах": "route_mgmt?action=cp_list",
|
||
"Связанные КП": relatedPointsHref(),
|
||
}
|
||
|
||
const catLinks = {
|
||
"Редактор маршрута": (catId) => `route_mgmt?action=edit&cat_id=${catId}`,
|
||
"Конструктор маршрута": (catId) => `route_mgmt/?action=build2&cat_id=${catId}`,
|
||
"Карта": (catId) => `route_mgmt?action=map&cat_id=${catId}`,
|
||
"Этапы": (catId) => `route_mgmt/?action=stages&cat_id=${catId}`,
|
||
"Легенда": (catId) => `route_mgmt?action=preview&cat_id=${catId}&locale_id=ru`,
|
||
}
|
||
|
||
const fileInputs = {
|
||
"attachment1": {
|
||
"l": "legend_photo",
|
||
"h": "history_photo"
|
||
},
|
||
"attachment4": {
|
||
"l": "legend_files",
|
||
"h": "history_files"
|
||
},
|
||
"attachment2": "admin_photo",
|
||
"attachment3": "admin_files"
|
||
}
|
||
|
||
const fileInpitVariants = {
|
||
"l": "для легенды",
|
||
"h": "для ист. справки"
|
||
}
|
||
|
||
const mapsCenterByCompetition = {
|
||
"msk2025": {
|
||
lat: 55.839808,
|
||
ifSouthern: {
|
||
lat: 55.798531,
|
||
lon: 37.690380,
|
||
},
|
||
ifEverywhere: {
|
||
lat: 55.839759,
|
||
lon: 37.706577
|
||
}
|
||
}
|
||
}
|
||
|
||
const localStorageItems = {
|
||
NEED_UPDATE_ID: "needUpdateId",
|
||
JUST_CREATED: "justCreated",
|
||
PREV_CREATED: "prevCreated",
|
||
LATTITUDE: "lattitude",
|
||
LONGITUDE: "longitude",
|
||
NOT_PRETTIFY_EDIT_CP: "notPrettifyEditCp",
|
||
REDIRECT_EXIT: "redirectExit",
|
||
DISABLED_PAGES: "disabledPages"
|
||
}
|
||
|
||
const ZOOM = 17
|
||
const METERS = 510
|
||
|
||
const stageColors = ["#ff8080", "#ffc680", "#ffff80", "#80ff80", "#80ffff", "#8080ff", "#ff80ff"]
|
||
|
||
class Tag {
|
||
static make(name, params = {}) {
|
||
let element = document.createElement(name)
|
||
for (const [name, value] of Object.entries(params)) {
|
||
if (name == "on") {
|
||
for (const [event, listener] of Object.entries(value)) {
|
||
element.addEventListener(event, listener)
|
||
}
|
||
}
|
||
else if (name == "classes") {
|
||
element.classList.add(...value.split(" "))
|
||
}
|
||
else if (name == "children") {
|
||
element.append(...value)
|
||
}
|
||
else if (name.startsWith("_")) {
|
||
element.setAttribute(name.substring(1), value)
|
||
}
|
||
else
|
||
element[name] = value
|
||
}
|
||
|
||
return element
|
||
}
|
||
|
||
static div(params) {
|
||
return this.make("div", params)
|
||
}
|
||
|
||
static span(params) {
|
||
return this.make("span", params)
|
||
}
|
||
|
||
static button(params) {
|
||
return this.make("button", params)
|
||
}
|
||
|
||
static input(params) {
|
||
return this.make("input", params)
|
||
}
|
||
|
||
static a(params) {
|
||
return this.make("a", params)
|
||
}
|
||
|
||
static img(params) {
|
||
return this.make("img", params)
|
||
}
|
||
}
|
||
|
||
class Property {
|
||
name
|
||
content
|
||
desc
|
||
|
||
constructor(row = null) {
|
||
if (row == null) return
|
||
|
||
let columns = [...row.querySelectorAll('td, th')].map(el => el.cloneNode(true))
|
||
if (columns.length === 1) {
|
||
this.content = columns[0].innerHTML.trim()
|
||
}
|
||
else if (columns.length >= 2) {
|
||
this.name = columns[0].innerHTML.trim()
|
||
let elementsToRemove = columns[1].querySelector("input[name=\"read_gps2\"]")
|
||
if (elementsToRemove) {
|
||
columns[1].removeChild(elementsToRemove)
|
||
}
|
||
|
||
this.content = columns[1].innerHTML
|
||
.replaceAll("<br>", "")
|
||
.replaceAll(/\s*Перейти к легенде.*$/gs, "")
|
||
.replaceAll(`общие ("c")`, "")
|
||
.replaceAll(`("l")`, "")
|
||
.replaceAll(`("h")`, "")
|
||
.replaceAll("Получить координаты КП из картинки", "")
|
||
.trim()
|
||
}
|
||
|
||
if (columns.length === 3) {
|
||
this.desc = columns[2].innerHTML.replaceAll("<br>", "").replaceAll(/(\n)\s*/g, "$1").trim()
|
||
}
|
||
}
|
||
|
||
toDiv(altName = null, altDesc = null) {
|
||
let desc = altDesc ?? this.desc
|
||
let descTooltip = desc ? Tag.div({
|
||
classes: 'desc-icon-container',
|
||
title: desc,
|
||
children: [
|
||
Tag.img({
|
||
classes: 'desc-icon',
|
||
src: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnIGlkPSJzdmcyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAwIiB3aWR0aD0iMjAwIiB2ZXJzaW9uPSIxLjAiPgogPHBhdGggaWQ9InBhdGgyMzgyIiBkPSJtMTY1LjMzIDExMy40NGExMDMuNjEgMTAzLjYxIDAgMSAxIC0yMDcuMjIgMCAxMDMuNjEgMTAzLjYxIDAgMSAxIDIwNy4yMiAweiIgdHJhbnNmb3JtPSJtYXRyaXgoLjkzNzM5IDAgMCAuOTM3MzkgNDIuMTQzIC02LjMzOTIpIiBzdHJva2Utd2lkdGg9IjAiIGZpbGw9IiNmZmYiLz4KIDxnIGlkPSJsYXllcjEiPgogIDxwYXRoIGlkPSJwYXRoMjQxMyIgZD0ibTEwMCAwYy01NS4yIDAtMTAwIDQ0LjgtMTAwIDEwMC01LjA0OTVlLTE1IDU1LjIgNDQuOCAxMDAgMTAwIDEwMHMxMDAtNDQuOCAxMDAtMTAwLTQ0LjgtMTAwLTEwMC0xMDB6bTAgMTIuODEyYzQ4LjEzIDAgODcuMTkgMzkuMDU4IDg3LjE5IDg3LjE4OHMtMzkuMDYgODcuMTktODcuMTkgODcuMTktODcuMTg4LTM5LjA2LTg3LjE4OC04Ny4xOSAzOS4wNTgtODcuMTg4IDg3LjE4OC04Ny4xODh6bTEuNDcgMjEuMjVjLTUuNDUgMC4wMy0xMC42NTMgMC43MzctMTUuMjgyIDIuMDYzLTQuNjk5IDEuMzQ2LTkuMTI2IDMuNDg0LTEyLjg3NiA2LjIxOS0zLjIzOCAyLjM2Mi02LjMzMyA1LjM5MS04LjY4NyA4LjUzMS00LjE1OSA1LjU0OS02LjQ2MSAxMS42NTEtNy4wNjMgMTguNjg3LTAuMDQgMC40NjgtMC4wNyAwLjg2OC0wLjA2MiAwLjg3NiAwLjAxNiAwLjAxNiAyMS43MDIgMi42ODcgMjEuODEyIDIuNjg3IDAuMDUzIDAgMC4xMTMtMC4yMzQgMC4yODItMC45MzcgMS45NDEtOC4wODUgNS40ODYtMTMuNTIxIDEwLjk2OC0xNi44MTMgNC4zMi0yLjU5NCA5LjgwOC0zLjYxMiAxNS43NzgtMi45NjkgMi43NCAwLjI5NSA1LjIxIDAuOTYgNy4zOCAyIDIuNzEgMS4zMDEgNS4xOCAzLjM2MSA2Ljk0IDUuODEzIDEuNTQgMi4xNTYgMi40NiA0LjU4NCAyLjc1IDcuMzEyIDAuMDggMC43NTkgMC4wNSAyLjQ4LTAuMDMgMy4yMTktMC4yMyAxLjgyNi0wLjcgMy4zNzgtMS41IDQuOTY5LTAuODEgMS41OTctMS40OCAyLjUxNC0yLjc2IDMuODEyLTIuMDMgMi4wNzctNS4xOCA0LjgyOS0xMC43OCA5LjQwNy0zLjYgMi45NDQtNi4wNCA1LjE1Ni04LjEyIDcuMzQzLTQuOTQzIDUuMTc5LTcuMTkxIDkuMDY5LTguNTY0IDE0LjcxOS0wLjkwNSAzLjcyLTEuMjU2IDcuNTUtMS4xNTYgMTMuMTkgMC4wMjUgMS40IDAuMDYyIDIuNzMgMC4wNjIgMi45N3YwLjQzaDIxLjU5OGwwLjAzLTIuNGMwLjAzLTMuMjcgMC4yMS01LjM3IDAuNTYtNy40MSAwLjU3LTMuMjcgMS40My01IDMuOTQtNy44MSAxLjYtMS44IDMuNy0zLjc2IDYuOTMtNi40NyA0Ljc3LTMuOTkxIDguMTEtNi45OSAxMS4yNi0xMC4xMjUgNC45MS00LjkwNyA3LjQ2LTguMjYgOS4yOC0xMi4xODcgMS40My0zLjA5MiAyLjIyLTYuMTY2IDIuNDYtOS41MzIgMC4wNi0wLjgxNiAwLjA3LTMuMDMgMC0zLjk2OC0wLjQ1LTcuMDQzLTMuMS0xMy4yNTMtOC4xNS0xOS4wMzItMC44LTAuOTA5LTIuNzgtMi44ODctMy43Mi0zLjcxOC00Ljk2LTQuMzk0LTEwLjY5LTcuMzUzLTE3LjU2LTkuMDk0LTQuMTktMS4wNjItOC4yMy0xLjYtMTMuMzUtMS43NS0wLjc4LTAuMDIzLTEuNTktMC4wMzYtMi4zNy0wLjAzMnptLTEwLjkwOCAxMDMuNnYyMmgyMS45OTh2LTIyaC0yMS45OTh6Ii8+CiA8L2c+Cjwvc3ZnPgo='
|
||
})
|
||
]
|
||
}) : ""
|
||
|
||
return Tag.div({
|
||
children: [
|
||
Tag.make("label", {
|
||
_for: '',
|
||
innerHTML: altName ?? this.name ?? ""
|
||
}),
|
||
Tag.div({
|
||
innerHTML: this.content ?? "",
|
||
}),
|
||
descTooltip
|
||
]
|
||
})
|
||
}
|
||
}
|
||
|
||
function createFrom(rows, classList, data) {
|
||
let container = Tag.div({
|
||
classes: classList
|
||
})
|
||
for (const options of data) {
|
||
let div
|
||
if (options.index != null) {
|
||
let prop = new Property(rows[options.index])
|
||
div = prop.toDiv(options.name, options.desc)
|
||
}
|
||
else {
|
||
let prop = new Property()
|
||
div = prop.toDiv(options.name)
|
||
}
|
||
container.append(div)
|
||
}
|
||
|
||
return container
|
||
}
|
||
|
||
function createFromMulti(rows, classList, data) {
|
||
let rowData = []
|
||
let toIndex = data.to ?? rows.length - 1
|
||
for (let i = data.from; i <= toIndex; i++) {
|
||
rowData.push({ index: i })
|
||
}
|
||
|
||
return createFrom(rows, classList, rowData)
|
||
}
|
||
|
||
function addCss(link) {
|
||
document.head.append(
|
||
Tag.make("link", {
|
||
href: link,
|
||
rel: "stylesheet"
|
||
})
|
||
)
|
||
}
|
||
|
||
function addJs(link) {
|
||
document.head.append(
|
||
Tag.make("script", {
|
||
src: link
|
||
})
|
||
)
|
||
}
|
||
|
||
function addStylesToHead(styles) {
|
||
document.head.append(
|
||
Tag.make("style", {
|
||
textContent: styles
|
||
})
|
||
)
|
||
}
|
||
|
||
const sleep = ms => new Promise(res => setTimeout(res, ms))
|
||
|
||
function pathNameSplit() {
|
||
return window.location.pathname.split('/')
|
||
}
|
||
|
||
function getCompetition() {
|
||
return pathNameSplit()[1]
|
||
}
|
||
|
||
function getPageType() {
|
||
return pathNameSplit()[2]
|
||
}
|
||
|
||
function urlParams() {
|
||
return new URLSearchParams(location.search)
|
||
}
|
||
|
||
function getAction() {
|
||
let params = urlParams()
|
||
return params.get("action")
|
||
}
|
||
|
||
function isCpManagement() {
|
||
return getPageType() === "cp_mgmt"
|
||
}
|
||
|
||
function isRouteManagement() {
|
||
return getPageType() === "route_mgmt"
|
||
}
|
||
|
||
function isCpListPage() {
|
||
return isCpManagement() && getAction() === null
|
||
}
|
||
|
||
function isCpEditPage() {
|
||
return isCpManagement() && getAction() === "edit"
|
||
}
|
||
|
||
function isCpDeletePage() {
|
||
return isCpManagement() && getAction() === "delete"
|
||
}
|
||
|
||
function isRouteListPage() {
|
||
return isRouteManagement() && getAction() === null
|
||
}
|
||
|
||
function isRouteBuildPage() {
|
||
return isRouteManagement() && getAction() === "build2"
|
||
}
|
||
|
||
function isRouteEditPage() {
|
||
return isRouteManagement() && getAction() === "edit"
|
||
}
|
||
|
||
function isRouteStagesPage() {
|
||
return isRouteManagement() && getAction() === "stages"
|
||
}
|
||
|
||
function isRouteMapPage() {
|
||
return isRouteManagement() && getAction() === "map"
|
||
}
|
||
|
||
function cpLink(cpId) {
|
||
return `/${getCompetition()}/cp_mgmt/?action=edit&cp_id=${cpId}`
|
||
}
|
||
|
||
async function updatePoint(formData) {
|
||
return await fetch(`https://${adminSite}/api/competitions/${getCompetition()}/update`, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
}
|
||
|
||
async function deletePoint() {
|
||
return await fetch(`https://${adminSite}/api/points/${urlParams().get("cp_id")}`, {
|
||
method: 'DELETE'
|
||
})
|
||
}
|
||
|
||
async function getPointsByField(field, values) {
|
||
let params = new URLSearchParams()
|
||
for (const value of values) {
|
||
params.append(`${field}[]`, value)
|
||
}
|
||
let response = await fetch(`https://${adminSite}/api/competitions/${getCompetition()}/points?${params.toString()}`, {
|
||
method: 'GET'
|
||
})
|
||
return await response.json()
|
||
}
|
||
|
||
async function updateRoute(formData) {
|
||
return await fetch(`https://${adminSite}/api/competitions/${getCompetition()}/route`, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
}
|
||
|
||
async function getRoutes() {
|
||
let response = await fetch(`https://${adminSite}/api/points/${urlParams().get("cp_id")}/routes`, {
|
||
method: 'GET'
|
||
})
|
||
return await response.json()
|
||
}
|
||
|
||
async function getUnknownPoints() {
|
||
let response = await fetch(`https://${adminSite}/api/competitions/${getCompetition()}/routes/${urlParams().get("cat_id")}/unknown`, {
|
||
method: 'GET'
|
||
})
|
||
return await response.json()
|
||
}
|
||
|
||
function relatedPointsHref() {
|
||
return `https://${adminSite}/competitions/${getCompetition()}/relations`
|
||
}
|
||
|
||
function createPseudoButton(button, callback, clickAfter = true) {
|
||
let pseudoButton = Tag.button({
|
||
type: "button",
|
||
textContent: button.value,
|
||
classes: [...button.classList].join(" ") + " pseudo-save",
|
||
on: {
|
||
click: async () => {
|
||
await callback()
|
||
if (clickAfter) {
|
||
button.click()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
button.style.display = "none"
|
||
button.parentElement.insertBefore(pseudoButton, button)
|
||
}
|
||
|
||
function copyCoordinates() {
|
||
return Tag.button({
|
||
classes: "copy-button",
|
||
on: {
|
||
click: async event => {
|
||
event.preventDefault()
|
||
let lat = document.querySelector(`input[name="cp[lattitude]"]`).value
|
||
let lon = document.querySelector(`input[name="cp[longitude]"]`).value
|
||
|
||
const text = new Blob([`${lat}, ${lon}`], { type: "text/plain" })
|
||
const data = new ClipboardItem({ "text/plain": text })
|
||
await navigator.clipboard.write([data])
|
||
}
|
||
},
|
||
children: [
|
||
Tag.img({
|
||
src: "https://upload.wikimedia.org/wikipedia/commons/a/aa/Bw_copy_icon_320x320.svg"
|
||
})
|
||
]
|
||
})
|
||
}
|
||
|
||
function yandexMaps(lat, lon, zoom) {
|
||
return `https://yandex.ru/maps/?ll=${lon}%2C${lat}&z=${zoom}`
|
||
}
|
||
|
||
function googleMaps(lat, lon, meters) {
|
||
return `https://www.google.ru/maps/@${lat},${lon},${meters}m`
|
||
}
|
||
|
||
function pastvu(lat, lon, zoom) {
|
||
return `https://pastvu.com/?g=${lat},${lon}&z=${zoom}&s=yandex&t=scheme&type=1`
|
||
}
|
||
|
||
function twoGis(lat, lon, zoom) {
|
||
return `https://2gis.ru?m=${lon}%2C${lat}%2F${zoom}`
|
||
}
|
||
|
||
function makeRef(linkCallback, iconSrc, zoom) {
|
||
return Tag.a({
|
||
href: linkCallback(lat, lon, zoom),
|
||
target: "_blank",
|
||
on: {
|
||
click: function (e) {
|
||
let lat = document.querySelector(`input[name="cp[lattitude]"]`).value
|
||
let lon = document.querySelector(`input[name="cp[longitude]"]`).value
|
||
this.href = linkCallback(lat, lon, zoom)
|
||
}
|
||
},
|
||
children: [
|
||
Tag.img({
|
||
src: iconSrc,
|
||
classes: "map-icon"
|
||
})
|
||
]
|
||
})
|
||
}
|
||
|
||
let makeCoordinatesLinks = (function () {
|
||
let executed = false
|
||
return function () {
|
||
if (executed) return
|
||
executed = true
|
||
|
||
let linksContainer = Tag.div({
|
||
children: [
|
||
copyCoordinates(),
|
||
makeRef(yandexMaps, "https://upload.wikimedia.org/wikipedia/commons/7/72/Yandex_Maps_icon.svg", ZOOM),
|
||
makeRef(googleMaps, "https://upload.wikimedia.org/wikipedia/commons/a/aa/Google_Maps_icon_%282020%29.svg", METERS),
|
||
makeRef(pastvu, "https://pastvu.com/coast-icon.png", ZOOM),
|
||
makeRef(twoGis, "https://d-assets.2gis.ru/favicon.png", ZOOM)
|
||
]
|
||
})
|
||
|
||
document.querySelector(`div:has(> div > input[name="cp[longitude]"])`).after(linksContainer)
|
||
}
|
||
})()
|
||
|
||
function makeDownloadLink(name, href = null) {
|
||
return Tag.a({
|
||
download: name,
|
||
textContent: "Скачать",
|
||
target: "_blank",
|
||
href: href
|
||
})
|
||
}
|
||
|
||
async function sendFileDeleted(inputName, fileInputVariant = null) {
|
||
let formData = new FormData()
|
||
|
||
let normalInputName = fileInputVariant !== null ? fileInputs[inputName][fileInputVariant] : fileInputs[inputName]
|
||
formData.set(`cp[id]`, document.querySelector("input[name='cp[id]']").value)
|
||
formData.set(`cp[${normalInputName}]`, "-1")
|
||
let result = await updatePoint(formData)
|
||
try {
|
||
console.log(result.ok)
|
||
}
|
||
catch (e) {
|
||
console.log(e)
|
||
}
|
||
}
|
||
|
||
function parsePurpose(cell) {
|
||
return cell?.querySelector("td:nth-child(2)")?.innerHTML.match(/для[^\|]+/g)?.[0].trim()
|
||
}
|
||
|
||
function parsefileInputVariant(cell) {
|
||
return Object.keys(fileInpitVariants).find(key => fileInpitVariants[key] === parsePurpose(cell))
|
||
}
|
||
|
||
function addDisplayedFile(inputName, type, file, preview, removeLink, removedFilesLinks, variant = null) {
|
||
if (!inputName || !file || !removeLink) return
|
||
|
||
let fileListContainer
|
||
if (variant !== null) {
|
||
if (inputName == "attachment1" || inputName == "attachment4") {
|
||
if (variant == "l") {
|
||
fileListContainer = document.querySelector(`[data-container="${fileInputs[inputName][variant]}"] .file-list-container`)
|
||
}
|
||
else if (variant == "h") {
|
||
let filesContainer = document.querySelector(`[data-container="${fileInputs[inputName].h}"]`)
|
||
fileListContainer = getFileListContainer(filesContainer)
|
||
}
|
||
}
|
||
}
|
||
|
||
let input = document.querySelector(`input[name="${inputName}\[\]"]`)
|
||
fileListContainer ??= input.parentElement.parentElement.querySelector(".file-list-container")
|
||
let fileContainer = Tag.div({
|
||
classes: "file-container"
|
||
})
|
||
|
||
const displayedFile = Tag.make(type ?? "a", {
|
||
classes: "preview-small"
|
||
})
|
||
|
||
if (type == "img") {
|
||
displayedFile.src = preview
|
||
displayedFile.dataset.origin = file
|
||
displayedFile.addEventListener("click", () => {
|
||
let dialog = document.querySelector("dialog")
|
||
makeSwiper(dialog, fileListContainer, file)
|
||
dialog.showModal()
|
||
})
|
||
}
|
||
else if (type == "audio") {
|
||
displayedFile.src = file
|
||
displayedFile.setAttribute("controls", "")
|
||
}
|
||
else {
|
||
displayedFile.href = file
|
||
displayedFile.textContent = "Файл"
|
||
}
|
||
|
||
let fileButtonsContainer = Tag.div({
|
||
classes: "file-buttons-container"
|
||
})
|
||
|
||
fileButtonsContainer.append(makeDownloadLink(/[^/]*$/.exec(new URL(file).pathname)[0], file))
|
||
|
||
let deleteButton = Tag.button({
|
||
type: "button",
|
||
classes: "button-delete",
|
||
textContent: "x",
|
||
on: {
|
||
click: async e => {
|
||
if (!confirm("Точно?")) return
|
||
|
||
await fetch(removeLink)
|
||
await sendFileDeleted(inputName, variant)
|
||
removedFilesLinks.push(removeLink)
|
||
|
||
fileListContainer.removeChild(fileContainer)
|
||
}
|
||
}
|
||
})
|
||
|
||
fileButtonsContainer.append(deleteButton)
|
||
|
||
fileContainer.append(displayedFile)
|
||
fileContainer.append(fileButtonsContainer)
|
||
|
||
fileListContainer.append(fileContainer)
|
||
}
|
||
|
||
function dataURLtoFile(dataurl, filename) {
|
||
let arr = dataurl.split(',')
|
||
let mime = arr[0].match(/:(.*?);/)[1]
|
||
let bstr = atob(arr[arr.length - 1])
|
||
let n = bstr.length
|
||
let u8arr = new Uint8Array(n)
|
||
while (n--) {
|
||
u8arr[n] = bstr.charCodeAt(n)
|
||
}
|
||
return new File([u8arr], filename, { type: mime })
|
||
}
|
||
|
||
function getHtmlElementByFileType(file) {
|
||
const htmlElementsForTypes = {
|
||
"image/": "img",
|
||
"audio/": "audio"
|
||
}
|
||
let res = "div"
|
||
for (const [type, el] of Object.entries(htmlElementsForTypes)) {
|
||
if (file.type.startsWith(type)) {
|
||
res = el
|
||
break
|
||
}
|
||
}
|
||
let el = Tag.make(res)
|
||
if (res == "audio") {
|
||
el.setAttribute("controls", "")
|
||
}
|
||
|
||
return el
|
||
}
|
||
|
||
function getAttachmentIndex(input) {
|
||
return input.name.replace(/\D/g, '')
|
||
}
|
||
|
||
function getContainersByVariant(variant, attachmentIndex) {
|
||
let invertedVariant = variant == "l" ? "h" : "l"
|
||
let variants = fileInputs[`attachment${attachmentIndex}`]
|
||
|
||
return [document.querySelector(`[data-container="${variants[invertedVariant]}"]`), document.querySelector(`[data-container="${variants[variant]}"]`)]
|
||
}
|
||
|
||
function makeFileListContainer() {
|
||
return Tag.div({
|
||
classes: "file-list-container"
|
||
})
|
||
}
|
||
|
||
function getFileListContainer(filesContainer) {
|
||
let fileListContainer = filesContainer.querySelector(".file-list-container")
|
||
if (fileListContainer == null) {
|
||
fileListContainer = makeFileListContainer()
|
||
filesContainer.append(fileListContainer)
|
||
}
|
||
|
||
return fileListContainer
|
||
}
|
||
|
||
function makeHandleFilesFunc() {
|
||
let storedFiles = []
|
||
|
||
return function () {
|
||
let dt = new DataTransfer()
|
||
|
||
if (storedFiles.length) {
|
||
for (const oldFile of storedFiles) {
|
||
let oldFileObj = dataURLtoFile(oldFile.data, oldFile.name)
|
||
dt.items.add(oldFileObj)
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < this.files.length; i++) {
|
||
const file = this.files[i]
|
||
dt.items.add(file)
|
||
|
||
let variant = this.parentElement.querySelector('input[type="radio"]:checked')?.value
|
||
let fileListContainer
|
||
|
||
if (variant) {
|
||
let [_, filesContainer] = getContainersByVariant(variant, getAttachmentIndex(this))
|
||
fileListContainer = getFileListContainer(filesContainer)
|
||
}
|
||
else {
|
||
fileListContainer = this.parentElement.parentElement.querySelector(".file-list-container")
|
||
}
|
||
|
||
let fileContainer = Tag.div({
|
||
classes: "file-container file-container-new"
|
||
})
|
||
|
||
const displayedFile = getHtmlElementByFileType(file)
|
||
displayedFile.classList.add("preview")
|
||
displayedFile.file = file
|
||
displayedFile.addEventListener("click", () => {
|
||
let dialog = document.querySelector("dialog")
|
||
makeSwiper(dialog, fileListContainer, displayedFile.src)
|
||
dialog.showModal()
|
||
})
|
||
|
||
let fileButtonsContainer = Tag.div({
|
||
classes: "file-buttons-container"
|
||
})
|
||
|
||
let downloadLink = makeDownloadLink(file.name)
|
||
fileButtonsContainer.append(downloadLink)
|
||
|
||
let deleteButton = Tag.button({
|
||
classes: "button-delete",
|
||
textContent: "x",
|
||
type: "button",
|
||
on: {
|
||
click: e => {
|
||
let index = [...fileContainer.parentElement.children].indexOf(fileContainer)
|
||
|
||
if (!confirm("Точно?")) return
|
||
|
||
storedFiles.splice(storedFiles.length - this.files.length + index, 1)
|
||
|
||
let rdt = new DataTransfer()
|
||
for (const file of this.files) {
|
||
rdt.items.add(file)
|
||
}
|
||
rdt.items.remove(index)
|
||
this.files = rdt.files
|
||
|
||
fileContainer.parentElement.removeChild(fileContainer)
|
||
}
|
||
}
|
||
})
|
||
|
||
fileButtonsContainer.append(deleteButton)
|
||
|
||
fileContainer.append(displayedFile)
|
||
fileContainer.append(fileButtonsContainer)
|
||
|
||
fileListContainer.append(fileContainer)
|
||
|
||
const reader = new FileReader()
|
||
reader.onload = (e) => {
|
||
displayedFile.src = e.target.result
|
||
downloadLink.setAttribute("href", displayedFile.src)
|
||
|
||
storedFiles.push({ data: displayedFile.src, name: file.name })
|
||
}
|
||
reader.readAsDataURL(file)
|
||
}
|
||
|
||
this.files = dt.files
|
||
}
|
||
}
|
||
|
||
let prettifyFiles = (function () {
|
||
let executed = false
|
||
return function (insertedFileRows, removedFilesLinks) {
|
||
if (executed) return
|
||
executed = true
|
||
|
||
document.querySelector("#new").querySelectorAll("input[type=file]").forEach((element, index) => {
|
||
element.id = `input-file-${index}`
|
||
element.setAttribute("multiple", "multiple")
|
||
element.dataset.index = index
|
||
let pseudoInput = Tag.make("label", {
|
||
classes: "custom-file-upload",
|
||
_for: element.id,
|
||
textContent: "+"
|
||
})
|
||
element.parentElement.insertBefore(pseudoInput, element)
|
||
|
||
element.parentElement.parentElement.append(makeFileListContainer())
|
||
|
||
element.addEventListener("change", makeHandleFilesFunc(), false)
|
||
})
|
||
|
||
moveFileInputValues(insertedFileRows, removedFilesLinks)
|
||
}
|
||
})()
|
||
|
||
function saveInputValues(from) {
|
||
let fromInputs = [...from.querySelectorAll("input, textarea")]
|
||
let result = new Map()
|
||
|
||
for (const fromEl of fromInputs) {
|
||
result.set(fromEl, fromEl.type == "radio" ? fromEl.checked : fromEl.value)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
function setInputValues(to, values) {
|
||
let toInputs = [...to.querySelectorAll("input, textarea")]
|
||
|
||
for (const [fromEl, value] of values) {
|
||
let currentToInput = toInputs.find(toEl =>
|
||
toEl.type !== 'file' &&
|
||
toEl.type === fromEl.type &&
|
||
toEl.name === fromEl.name &&
|
||
toEl.id === fromEl.id &&
|
||
(toEl.type !== "radio" || toEl.value === fromEl.value)
|
||
)
|
||
|
||
if (currentToInput) {
|
||
currentToInput.checked = fromEl.checked
|
||
currentToInput.value = fromEl.value
|
||
}
|
||
}
|
||
}
|
||
|
||
function moveFileInputValues(insertedFileRows, removedFilesLinks) {
|
||
for (const [inputRowIndex, rows] of insertedFileRows) {
|
||
for (const row of rows) {
|
||
let fileInput = null, type = null, fileUrl = null, deleteUrl = null, preview = null
|
||
if (row.querySelector("img")) {
|
||
type = "img"
|
||
fileUrl = row.querySelector("td:first-child a").href
|
||
preview = row.querySelector("img").src
|
||
}
|
||
else if (row.querySelector("audio")) {
|
||
type = "audio"
|
||
fileUrl = row.querySelector("audio").src
|
||
}
|
||
else {
|
||
fileUrl = row.querySelector("td:first-child a").href
|
||
}
|
||
|
||
deleteUrl = row.querySelector("td:nth-child(2) a").href
|
||
|
||
if (inputRowIndex == 23) {
|
||
fileInput = "attachment1"
|
||
}
|
||
else if (inputRowIndex == 27) {
|
||
fileInput = "attachment4"
|
||
}
|
||
else if (inputRowIndex == 31) {
|
||
fileInput = "attachment2"
|
||
}
|
||
else if (inputRowIndex == 35) {
|
||
fileInput = "attachment3"
|
||
}
|
||
else {
|
||
console.log(inputRowIndex)
|
||
return
|
||
}
|
||
|
||
let variant = parsefileInputVariant(row)
|
||
|
||
addDisplayedFile(fileInput, type, fileUrl, preview, deleteUrl, removedFilesLinks, variant)
|
||
}
|
||
}
|
||
}
|
||
|
||
function moveNewFilesOnVariantChange() {
|
||
document.querySelectorAll(`input[name^="new_file_type"]`).forEach(el => el.addEventListener("change", function () {
|
||
let attachmentIndex = getAttachmentIndex(this)
|
||
|
||
let [from, to] = getContainersByVariant(this.value, attachmentIndex)
|
||
let newFiles = [...from.querySelectorAll(`.file-container-new`)]
|
||
for (const newFile of newFiles) {
|
||
getFileListContainer(to).append(newFile)
|
||
}
|
||
}))
|
||
}
|
||
|
||
function removeDeletedFilesFromUglyVersion(removeLink) {
|
||
let shortHref = removeLink.split("/").at(-1)
|
||
document.querySelector(`tr:has(a[href="${shortHref}"])`).remove()
|
||
}
|
||
|
||
function moveInputValues(form, was, became) {
|
||
let inputValues = saveInputValues(was)
|
||
form.removeChild(was)
|
||
form.append(became)
|
||
setInputValues(became, inputValues)
|
||
createSendButtons()
|
||
}
|
||
|
||
function prettifyEditCpForm(form, was, became, insertedFileRows, removedFilesLinks) {
|
||
moveInputValues(form, was, became)
|
||
prettifyFiles(insertedFileRows, removedFilesLinks)
|
||
|
||
moveNewFilesOnVariantChange()
|
||
$("#cps_main").select2()
|
||
makeCoordinatesLinks()
|
||
}
|
||
|
||
function uglifyEditCpForm(form, was, became, removedFilesLinks) {
|
||
moveInputValues(form, was, became)
|
||
for (const removedFileLink of removedFilesLinks) {
|
||
removeDeletedFilesFromUglyVersion(removedFileLink)
|
||
}
|
||
removedFilesLinks.length = 0
|
||
}
|
||
|
||
function createAlwaysPrettifyInput(index) {
|
||
let alwaysPrettify = Tag.div({
|
||
classes: "always-prettify-container"
|
||
})
|
||
|
||
let alwaysPrettifyLabel = Tag.make("label", {
|
||
_for: "always-prettify-" + index,
|
||
textContent: "Всегда"
|
||
})
|
||
|
||
alwaysPrettify.append(alwaysPrettifyLabel)
|
||
|
||
let alwaysPrettifyCheckbox = Tag.input({
|
||
type: "checkbox",
|
||
id: "always-prettify-" + index,
|
||
name: "always-prettify-" + index,
|
||
on: {
|
||
change: function () {
|
||
let otherCheckboxes = document.querySelectorAll(`input[name^="always-prettify-"]`)
|
||
for (let checkbox of otherCheckboxes) {
|
||
if (checkbox.id !== this.id) {
|
||
checkbox.checked = this.checked
|
||
}
|
||
}
|
||
if (this.checked)
|
||
localStorage.removeItem(localStorageItems.NOT_PRETTIFY_EDIT_CP)
|
||
else
|
||
localStorage.setItem(localStorageItems.NOT_PRETTIFY_EDIT_CP, "+")
|
||
}
|
||
}
|
||
})
|
||
|
||
alwaysPrettify.append(alwaysPrettifyCheckbox)
|
||
|
||
return alwaysPrettify
|
||
}
|
||
|
||
function hide(elList) {
|
||
elList.forEach(el => el.classList.add("hidden"))
|
||
}
|
||
|
||
function show(elList) {
|
||
elList.forEach(el => el.classList.remove("hidden"))
|
||
}
|
||
|
||
function toggleRows(rows, from, to, cycle, showIndex) {
|
||
for (let i = from; i < to; i += cycle) {
|
||
for (let j = 0; j < cycle; j++) {
|
||
if (!showIndex.includes(j)) {
|
||
rows[i + j]?.classList.toggle("collapsed")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function toggleText(current, ...variants) {
|
||
let index = variants.indexOf(current)
|
||
if (index == -1) return null
|
||
|
||
return variants[index + 1 < variants.length ? index + 1 : 0]
|
||
}
|
||
|
||
function makeSwiper(dialog, fileListContainer, src) {
|
||
let files = [...fileListContainer.querySelectorAll(`:is(.preview, .preview-small)`)]
|
||
|
||
let swiperWrapper = Tag.div({
|
||
classes: "swiper-wrapper"
|
||
})
|
||
|
||
for (const file of files) {
|
||
let swiperSlide = Tag.div({
|
||
classes: "swiper-slide",
|
||
children: [
|
||
Tag.a({
|
||
classes: "swiper-download-link",
|
||
href: file.dataset.origin,
|
||
text: "Скачать",
|
||
target: "_blank"
|
||
}),
|
||
Tag.img({
|
||
src: file.dataset.origin
|
||
})
|
||
]
|
||
})
|
||
|
||
swiperWrapper.append(swiperSlide)
|
||
}
|
||
|
||
let swiperDiv = Tag.div({
|
||
classes: "swiper",
|
||
children: [
|
||
swiperWrapper,
|
||
Tag.div({
|
||
classes: "swiper-button-prev"
|
||
}),
|
||
Tag.div({
|
||
classes: "swiper-button-next"
|
||
})
|
||
]
|
||
})
|
||
|
||
dialog.append(swiperDiv)
|
||
|
||
new Swiper('.swiper', {
|
||
initialSlide: files.findIndex(el => el.dataset.origin && el.dataset.origin === src || el.src === src),
|
||
|
||
navigation: {
|
||
nextEl: '.swiper-button-next',
|
||
prevEl: '.swiper-button-prev',
|
||
},
|
||
})
|
||
|
||
bindArrowsForGallery('.swiper-button-prev', '.swiper-button-next')
|
||
}
|
||
|
||
const bindArrowsForGallery = (function () {
|
||
let executed = false
|
||
return function(leftButtonQuery, rightButtonQuery) {
|
||
if (executed) return
|
||
executed = true
|
||
|
||
document.addEventListener("keydown", function (e) {
|
||
if (!document.querySelector("dialog").open) return
|
||
switch (e.key) {
|
||
case "ArrowLeft":
|
||
document.querySelector(leftButtonQuery).click()
|
||
break
|
||
|
||
case "ArrowRight":
|
||
document.querySelector(rightButtonQuery).click()
|
||
break
|
||
|
||
default: return
|
||
}
|
||
e.preventDefault()
|
||
})
|
||
}
|
||
})()
|
||
|
||
async function sendForm() {
|
||
let formData = new FormData(document.querySelector("form"))
|
||
|
||
let fileInputNames = Object.values(fileInputs).reduce((res, el) => typeof el == "string" ? [...res, el] : [...res, ...Object.values(el)], [])
|
||
for (const fileInput of fileInputNames) {
|
||
let fileListContainer = document.querySelector(`[data-container=${fileInput}] .file-list-container`)
|
||
if (fileListContainer) {
|
||
let count = [...fileListContainer.children].filter(el => !el.classList.contains("file-container-new")).length
|
||
formData.set(`cp[${fileInput}]`, count)
|
||
}
|
||
}
|
||
|
||
if (formData.get("cp[id]") == '') {
|
||
localStorage.setItem(localStorageItems.NEED_UPDATE_ID, true)
|
||
}
|
||
|
||
let props = document.querySelectorAll("input[name^=\"prop_\"]")
|
||
for (let prop of props) {
|
||
let parent = prop.parentElement
|
||
let propName
|
||
|
||
if (parent.tagName === "DIV") {
|
||
propName = parent.parentElement.querySelector("label").textContent
|
||
}
|
||
else if (parent.tagName === "TD") {
|
||
propName = parent.parentElement.querySelector("td:first-child").textContent
|
||
}
|
||
|
||
formData.set(`propname[${prop.name}]`, propName)
|
||
}
|
||
|
||
let result = await updatePoint(formData)
|
||
|
||
try {
|
||
console.log(result.ok)
|
||
}
|
||
catch (e) {
|
||
console.log(e)
|
||
}
|
||
}
|
||
|
||
function createSaveAndNewButton() {
|
||
let saveAndNewButton = Tag.button({
|
||
textContent: "+",
|
||
type: "button",
|
||
classes: "safe-action",
|
||
on: {
|
||
click: async () => {
|
||
await sendForm()
|
||
|
||
let cpNumber = document.querySelector(`input[name="cp[number]"]`).value
|
||
let lattitude = document.querySelector(`input[name="cp[lattitude]"]`).value
|
||
let longitude = document.querySelector(`input[name="cp[longitude]"]`).value
|
||
|
||
localStorage.setItem(localStorageItems.JUST_CREATED, cpNumber)
|
||
localStorage.setItem(localStorageItems.LATTITUDE, lattitude)
|
||
localStorage.setItem(localStorageItems.LONGITUDE, longitude)
|
||
|
||
document.querySelector(`input[name="save_go"]`).click()
|
||
}
|
||
}
|
||
})
|
||
|
||
return saveAndNewButton
|
||
}
|
||
|
||
function createSendButtons() {
|
||
if (document.querySelector(".pseudo-save")) return
|
||
|
||
let saveAndStayButtons = document.querySelectorAll(`input[name="save_go"]`)
|
||
let saveAndExitButtons = document.querySelectorAll(`input[name="save_exit"]`)
|
||
let saveButtons = [...saveAndStayButtons, ...saveAndExitButtons]
|
||
let cpIsKnown = document.querySelector(`input[name="cp[id]"]`).value != ''
|
||
|
||
for (let saveButton of saveButtons) {
|
||
createPseudoButton(saveButton, async () => {
|
||
await sendForm()
|
||
|
||
if (cpIsKnown) return
|
||
|
||
if (saveButton.name == "save_exit") {
|
||
localStorage.setItem(localStorageItems.REDIRECT_EXIT, true)
|
||
}
|
||
|
||
saveAndStayButtons[0].click()
|
||
}, cpIsKnown)
|
||
}
|
||
|
||
saveAndStayButtons.forEach(el => el.after(createSaveAndNewButton()))
|
||
}
|
||
|
||
function redirectAfterNewCpIfNeeded() {
|
||
let justCreated = localStorage.getItem(localStorageItems.JUST_CREATED)
|
||
let prevCreated = localStorage.getItem(localStorageItems.PREV_CREATED)
|
||
let needUpdateId = localStorage.getItem(localStorageItems.NEED_UPDATE_ID)
|
||
let exit = localStorage.getItem(localStorageItems.REDIRECT_EXIT)
|
||
|
||
if (needUpdateId) {
|
||
let formData = new FormData()
|
||
formData.set("cp[id]", urlParams().get("cp_id"))
|
||
formData.set("cp[number]", document.querySelector(`input[name="cp[number]"]`).value)
|
||
formData.set("update_id", true)
|
||
updatePoint(formData).then(() => localStorage.removeItem(localStorageItems.NEED_UPDATE_ID))
|
||
}
|
||
|
||
if (exit) {
|
||
localStorage.removeItem(localStorageItems.REDIRECT_EXIT)
|
||
|
||
location.href = location.href.replace(location.search, '')
|
||
return
|
||
}
|
||
|
||
if (justCreated !== null) {
|
||
localStorage.setItem(localStorageItems.PREV_CREATED, justCreated)
|
||
localStorage.removeItem(localStorageItems.JUST_CREATED)
|
||
|
||
location.href = location.href.replace(location.search, "?action=edit")
|
||
return
|
||
}
|
||
if (prevCreated !== null) {
|
||
document.querySelector(`input[name="cp[number]"]`).value = parseInt(localStorage.getItem(localStorageItems.PREV_CREATED)) + 1
|
||
document.querySelector(`input[name="cp[lattitude]"]`).value = localStorage.getItem(localStorageItems.LATTITUDE)
|
||
document.querySelector(`input[name="cp[longitude]"]`).value = localStorage.getItem(localStorageItems.LONGITUDE)
|
||
|
||
localStorage.removeItem(localStorageItems.PREV_CREATED)
|
||
localStorage.removeItem(localStorageItems.LATTITUDE)
|
||
localStorage.removeItem(localStorageItems.LONGITUDE)
|
||
}
|
||
}
|
||
|
||
function addUglyDeleteListener() {
|
||
let links = [...document.querySelectorAll("a[href*='del_stored']")]
|
||
|
||
for (const link of links) {
|
||
let shortHref = link.href.split("/").at(-1)
|
||
let fileInput = document.querySelector(`tr:has(a[href$="${shortHref}"]) ~ tr:has(input[type="file"]) input[type="file"]`)
|
||
let fileInputVariant = parsefileInputVariant(document.querySelector(`tr:has(a[href$="${shortHref}"])`))
|
||
|
||
link.addEventListener("click", async e => {
|
||
e.preventDefault()
|
||
if (!confirm("Точно?")) return
|
||
|
||
await sendFileDeleted(fileInput.name.split("[")[0], fileInputVariant)
|
||
location.href = link.href
|
||
})
|
||
}
|
||
}
|
||
|
||
function createMenuFromLinks(links, blanks = []) {
|
||
let menu = Tag.make("menu")
|
||
|
||
for (const [label, href] of Object.entries(links)) {
|
||
let menuItem = Tag.make("li")
|
||
let link = Tag.a({
|
||
href: href,
|
||
innerText: label
|
||
})
|
||
|
||
if (blanks.includes(label)) {
|
||
link.target = "_blank"
|
||
}
|
||
|
||
menuItem.append(link)
|
||
menu.append(menuItem)
|
||
}
|
||
|
||
return menu
|
||
}
|
||
|
||
function addStickyMenu() {
|
||
let competition = getCompetition()
|
||
if (!competition) return
|
||
|
||
let menuContainer = Tag.make("nav", {
|
||
classes: "sticky-menu"
|
||
})
|
||
|
||
let mainLinksFormatted = {}
|
||
for (const [label, href] of Object.entries(links)) {
|
||
mainLinksFormatted[label] = href.startsWith("http") ? href : `/${competition}/${href}`
|
||
}
|
||
menuContainer.append(createMenuFromLinks(mainLinksFormatted, ["Легенда"]))
|
||
|
||
let goToContainer = Tag.make("li", {
|
||
classes: "go-to-container",
|
||
children: [
|
||
Tag.input({
|
||
type: "text",
|
||
id: "go-to-cp"
|
||
}),
|
||
Tag.span({
|
||
textContent: "✏️ КП",
|
||
on: {
|
||
click: async function () {
|
||
let number = document.querySelector("#go-to-cp").value
|
||
let json = await getPointsByField("number", [number])
|
||
let id = json[number].cp_id
|
||
|
||
if (id == null) {
|
||
alert("Нет КП с таким номером")
|
||
return
|
||
}
|
||
|
||
location.href = cpLink(id)
|
||
}
|
||
}
|
||
})
|
||
]
|
||
})
|
||
|
||
;[...menuContainer.querySelectorAll(`li`)].find(el => el.querySelector(`a`).textContent.trim() == "Контрольные пункты")?.after(goToContainer)
|
||
|
||
let catId = urlParams().get("cat_id")
|
||
if (catId !== null) {
|
||
let catLinksFormatted = {}
|
||
for (const [label, href] of Object.entries(catLinks)) {
|
||
catLinksFormatted[label] = href(catId)
|
||
}
|
||
|
||
menuContainer.append(createMenuFromLinks(catLinksFormatted))
|
||
}
|
||
|
||
document.querySelector("#header").after(menuContainer)
|
||
|
||
addStylesToHead(`
|
||
.sticky-menu {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 9999;
|
||
|
||
menu {
|
||
display: flex;
|
||
gap: .3rem;
|
||
background: white;
|
||
font-size: 1.2rem;
|
||
padding-left: 10px;
|
||
|
||
li {
|
||
list-style-type: none;
|
||
|
||
&:not(:last-child)::after {
|
||
content: " | "
|
||
}
|
||
}
|
||
}
|
||
|
||
.go-to-container {
|
||
input {
|
||
width: 5em;
|
||
}
|
||
|
||
span {
|
||
margin-left: .5em;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
}
|
||
`)
|
||
}
|
||
|
||
function addClearBoth() {
|
||
let clearEl = Tag.div()
|
||
clearEl.style.clear = "both"
|
||
document.body.append(clearEl)
|
||
}
|
||
|
||
function hasMap() {
|
||
return document.querySelector(".leaflet-container") !== null
|
||
}
|
||
|
||
function initMapbox() {
|
||
addJs('https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js')
|
||
addCss('https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css')
|
||
}
|
||
|
||
function addFullscreenButton() {
|
||
$(async () => {
|
||
if (document.querySelector("#map") !== null) {
|
||
while (L.Control.Fullscreen === undefined) {
|
||
await sleep(500)
|
||
}
|
||
map.addControl(new L.Control.Fullscreen())
|
||
}
|
||
})
|
||
}
|
||
|
||
function addStageLink() {
|
||
let linkCells = [...document.querySelectorAll("#content table td:nth-child(2)")]
|
||
for (const linkCell of linkCells) {
|
||
let href = linkCell.querySelector("a").href
|
||
let catId = new URLSearchParams(href).get("cat_id")
|
||
let constructorLink = linkCell.querySelector(`a[href="${catLinks["Конструктор маршрута"](catId).replace("route_mgmt/", "")}"]`)
|
||
|
||
if (constructorLink) {
|
||
let innerText = "Этапы"
|
||
let stagesLink = Tag.a({
|
||
innerText: innerText,
|
||
href: catLinks[innerText](catId)
|
||
})
|
||
|
||
constructorLink.after(stagesLink)
|
||
constructorLink.after(". ")
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideNonTesters() {
|
||
if (urlParams().get("test") == null) return
|
||
|
||
document.querySelectorAll(`td:nth-child(2)`).forEach(el => {
|
||
let innerHTML = el.innerHTML
|
||
let from = innerHTML.indexOf("Отчёты тестировщиков")
|
||
if (from == -1)
|
||
from = innerHTML.length
|
||
|
||
el.innerHTML = innerHTML.substring(from)
|
||
})
|
||
}
|
||
|
||
function getRows() {
|
||
return [...document.querySelectorAll('#props > tbody > tr')]
|
||
}
|
||
|
||
function getInsertedFileRows(rows) {
|
||
let insertedFileRows = new Map()
|
||
let i = 0
|
||
while (i < rows.length) {
|
||
if ([23, 27, 31, 35].includes(i) && rows[i].querySelector(`input[type="file"]`) == null) {
|
||
if (!insertedFileRows.has(i)) {
|
||
insertedFileRows.set(i, [rows[i]])
|
||
}
|
||
else {
|
||
let savedRows = insertedFileRows.get(i)
|
||
savedRows.push(rows[i])
|
||
insertedFileRows.set(i, savedRows)
|
||
}
|
||
rows.splice(i, 1)
|
||
}
|
||
else {
|
||
i++
|
||
}
|
||
}
|
||
|
||
return insertedFileRows
|
||
}
|
||
|
||
function makeContainer() {
|
||
return Tag.div({
|
||
id: "new"
|
||
})
|
||
}
|
||
|
||
function makeHeader(rows) {
|
||
let headerContainer = createFrom(rows, "header", [
|
||
{ index: 2, name: "№", desc: "" },
|
||
{ index: 8, name: "Название" }
|
||
])
|
||
|
||
let copyLink = Tag.div({
|
||
innerHTML: new Property(rows[2]).desc
|
||
})
|
||
headerContainer.append(copyLink)
|
||
|
||
return headerContainer
|
||
}
|
||
|
||
function bindDeleteButton() {
|
||
createPseudoButton(document.querySelector(`input[name="delete_go"]`), deletePoint)
|
||
}
|
||
|
||
function makeTopAndBottomButtons(rows, form, oldTable, container, insertedFileRows, removedFilesLinks) {
|
||
let topButtonsContainer = createFrom(rows, "buttons", [
|
||
{ index: 0 }
|
||
])
|
||
|
||
let bottomButtonsContainer = topButtonsContainer.cloneNode(true)
|
||
|
||
document.querySelectorAll("#props tr:is(:first-child, :last-child) th").forEach((el, index) => {
|
||
let rowContentWrapper = Tag.div({
|
||
classes: "buttons-row__content-wrapper"
|
||
})
|
||
while ([...el.children].length > 0) {
|
||
let child = el.firstChild
|
||
rowContentWrapper.append(child)
|
||
}
|
||
|
||
let prettifyButton = Tag.button({
|
||
type: "button",
|
||
textContent: "Сделать красиво",
|
||
on: {
|
||
click: () => {
|
||
prettifyEditCpForm(form, oldTable, container, insertedFileRows, removedFilesLinks)
|
||
}
|
||
}
|
||
})
|
||
|
||
rowContentWrapper.append(prettifyButton)
|
||
rowContentWrapper.append(createAlwaysPrettifyInput(index))
|
||
|
||
el.append(rowContentWrapper)
|
||
})
|
||
|
||
;[topButtonsContainer, bottomButtonsContainer].forEach((el, index) => {
|
||
let unglifyButton = Tag.button({
|
||
type: "button",
|
||
textContent: "Сделать как было",
|
||
on: {
|
||
"click": () => {
|
||
uglifyEditCpForm(form, container, oldTable, removedFilesLinks)
|
||
}
|
||
}
|
||
})
|
||
|
||
let topRowContentWrapper = el.querySelector("div > div > div")
|
||
topRowContentWrapper.append(unglifyButton)
|
||
topRowContentWrapper.append(createAlwaysPrettifyInput(index))
|
||
})
|
||
|
||
return [topButtonsContainer, bottomButtonsContainer]
|
||
}
|
||
|
||
function makeTopOptions(rows) {
|
||
return createFrom(rows, "options", [
|
||
{ index: 3 },
|
||
{ index: 4, name: "КП-загадка" },
|
||
{ index: 16, name: "Этапник" },
|
||
{ index: 7, name: "Пиктограмма" },
|
||
{ index: 12, name: "Нужна ИС" },
|
||
{ index: 17, name: "Спрятать ИС" },
|
||
{ index: 14, name: "Основной КП" },
|
||
{ index: 10, name: "Широта" },
|
||
{ index: 11, name: "Долгота" },
|
||
{ index: 5, name: "Старт" },
|
||
{ index: 6, name: "Финиш" },
|
||
{ index: 15, name: "Знак" }
|
||
])
|
||
}
|
||
|
||
function makeComment(rows) {
|
||
return createFrom(rows, "comment", [
|
||
{ index: 9, desc: "" }
|
||
])
|
||
}
|
||
|
||
function makeLegend(rows) {
|
||
let legendContainer = Tag.div({
|
||
classes: "legend-container"
|
||
})
|
||
|
||
/* LEGEND DESC */
|
||
|
||
const LEGEND_RU_LABEL = "Русский"
|
||
const LEGEND_EN_LABEL = "Английский"
|
||
|
||
let legendDescHeader = Tag.div({
|
||
classes: "legend-container__desc-header"
|
||
})
|
||
|
||
let legendLang = Tag.div({
|
||
classes: "legend-desc__lang",
|
||
textContent: LEGEND_RU_LABEL
|
||
})
|
||
legendDescHeader.append(legendLang)
|
||
|
||
let legendEnSwitchContainer = createFrom(rows, "legend-switch-container", [
|
||
{ index: 48 }
|
||
])
|
||
legendEnSwitchContainer.addEventListener("click", event => {
|
||
hide([legendRuDescContainer, legendRuHiddenDescContainer, legendEnSwitchContainer])
|
||
show([legendEnDescContainer, legendEnHiddenDescContainer, legendRuSwitchContainer])
|
||
legendLang.textContent = LEGEND_EN_LABEL
|
||
})
|
||
legendDescHeader.append(legendEnSwitchContainer)
|
||
|
||
let copyDescButton = Tag.button({
|
||
type: "button",
|
||
textContent: "Копировать",
|
||
on: {
|
||
click: () => {
|
||
let ruInputs = [...container.querySelectorAll(":is(input, textarea)[name^=\"cp_strings\[ru\]\"]")]
|
||
let enInputs = [...container.querySelectorAll(":is(input, textarea)[name^=\"cp_strings\[en\]\"]")]
|
||
for (const [i, enInput] of enInputs.entries()) {
|
||
enInput.value = ruInputs[i].value
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
let legendRuSwitchContainer = createFrom(rows, "legend-switch-container hidden", [
|
||
{ index: 39 }
|
||
])
|
||
legendRuSwitchContainer.addEventListener("click", event => {
|
||
hide([legendEnDescContainer, legendEnHiddenDescContainer, legendRuSwitchContainer])
|
||
show([legendRuDescContainer, legendRuHiddenDescContainer, legendEnSwitchContainer])
|
||
legendLang.textContent = LEGEND_RU_LABEL
|
||
})
|
||
legendDescHeader.append(legendRuSwitchContainer)
|
||
|
||
legendDescHeader.append(copyDescButton)
|
||
|
||
let legendRuDescContainer = createFrom(rows, "legend-desc", [
|
||
{ index: 40, desc: "" },
|
||
{ index: 41, desc: "" },
|
||
{ index: 42, desc: "" },
|
||
{ index: 43, desc: "" }
|
||
])
|
||
|
||
let legendRuHiddenDescContainer = createFrom(rows, "legend-desc collapsible collapsed", [
|
||
{ index: 44, desc: "" },
|
||
{ index: 45, desc: "" },
|
||
{ index: 46, desc: "" },
|
||
{ index: 47, desc: "" },
|
||
])
|
||
|
||
let legendEnDescContainer = createFrom(rows, "legend-desc hidden", [
|
||
{ index: 49, desc: "" },
|
||
{ index: 50, desc: "" },
|
||
{ index: 51, desc: "" },
|
||
{ index: 52, desc: "" }
|
||
])
|
||
|
||
let legendEnHiddenDescContainer = createFrom(rows, "legend-desc collapsible collapsed hidden", [
|
||
{ index: 53, desc: "" },
|
||
{ index: 54, desc: "" },
|
||
{ index: 55, desc: "" },
|
||
{ index: 56, desc: "" }
|
||
])
|
||
|
||
let hider = Tag.div({
|
||
children: [
|
||
Tag.button({
|
||
classes: "collapse-button",
|
||
type: "button",
|
||
on: {
|
||
click: event => {
|
||
let container = document.querySelector('.legend-container')
|
||
container.querySelectorAll(".legend-desc.collapsible").forEach(element => {
|
||
element.classList.toggle("collapsed")
|
||
})
|
||
}
|
||
}
|
||
})
|
||
]
|
||
})
|
||
|
||
let legendDescContainer = Tag.div({
|
||
classes: "legend-container__desc",
|
||
children: [
|
||
legendDescHeader,
|
||
legendRuDescContainer,
|
||
legendRuHiddenDescContainer,
|
||
legendEnDescContainer,
|
||
legendEnHiddenDescContainer,
|
||
hider
|
||
]
|
||
})
|
||
|
||
legendContainer.append(legendDescContainer)
|
||
|
||
return legendContainer
|
||
}
|
||
|
||
function chooseLegendAsDefaultVariant() {
|
||
document.querySelector(`#props input[name="new_file_type1"][value="l"]`).click()
|
||
document.querySelector(`#props input[name="new_file_type4"][value="l"]`).click()
|
||
}
|
||
|
||
function makeLegendFiles(rows) {
|
||
let legendFilesContainer = Tag.div({
|
||
classes: "legend-container__files"
|
||
})
|
||
|
||
let imagesForLegendContainer = createFrom(rows, "files-container", [
|
||
{ index: 23, name: "Фото в легенде" }
|
||
])
|
||
imagesForLegendContainer.dataset.container = fileInputs.attachment1.l
|
||
legendFilesContainer.append(imagesForLegendContainer)
|
||
|
||
let imagesForHistoryContainer = createFrom(rows, "files-container", [
|
||
{ name: "Фото для ИС" }
|
||
])
|
||
imagesForHistoryContainer.dataset.container = fileInputs.attachment1.h
|
||
legendFilesContainer.append(imagesForHistoryContainer)
|
||
|
||
let audioForLegendContainer = createFrom(rows, "files-container", [
|
||
{ index: 27, name: "Файлы в легенде" }
|
||
])
|
||
audioForLegendContainer.dataset.container = fileInputs.attachment4.l
|
||
legendFilesContainer.append(audioForLegendContainer)
|
||
|
||
let audioForHistoryContainer = createFrom(rows, "files-container", [
|
||
{ name: "Файлы для ИС" }
|
||
])
|
||
audioForHistoryContainer.dataset.container = fileInputs.attachment4.h
|
||
legendFilesContainer.append(audioForHistoryContainer)
|
||
|
||
return legendFilesContainer
|
||
}
|
||
|
||
function makeAdminFiles(rows) {
|
||
let adminFilesContainer = Tag.div({
|
||
classes: 'admin-files-container'
|
||
})
|
||
|
||
let imagesForAdminContainer = createFrom(rows, "files-container admin-photo-container", [
|
||
{ index: 31, name: "Фото в админке" }
|
||
])
|
||
imagesForAdminContainer.dataset.container = fileInputs.attachment2
|
||
adminFilesContainer.append(imagesForAdminContainer)
|
||
|
||
let audioForAdminContainer = createFrom(rows, "files-container admin-files-container", [
|
||
{ index: 35, name: "Файлы в админке" }
|
||
])
|
||
audioForAdminContainer.dataset.container = fileInputs.attachment3
|
||
adminFilesContainer.append(audioForAdminContainer)
|
||
|
||
return adminFilesContainer
|
||
}
|
||
|
||
function makeBottomOptions(rows) {
|
||
return createFromMulti(rows, "options bottom-options", {
|
||
from: 59,
|
||
to: rows.length - 3
|
||
})
|
||
}
|
||
|
||
function makeDialog() {
|
||
let dialog = Tag.make("dialog", {
|
||
id: "dialog",
|
||
on: {
|
||
click: e => {
|
||
if (e.target == dialog) {
|
||
e.target.close()
|
||
}
|
||
},
|
||
close: e => {
|
||
e.target.innerHTML = ""
|
||
}
|
||
}
|
||
})
|
||
document.body.append(dialog)
|
||
}
|
||
|
||
function formatMap() {
|
||
if (map !== undefined && L !== undefined) {
|
||
let content = document.querySelector("#content")
|
||
let contentWrapper = Tag.div({
|
||
id: "content-wrapper",
|
||
children: [
|
||
document.querySelector("form"),
|
||
document.querySelector("#map-wrapper")
|
||
]
|
||
})
|
||
content.append(contentWrapper)
|
||
|
||
let panToCenter = Tag.button({
|
||
type: "button",
|
||
textContent: "В центр",
|
||
on: {
|
||
click: () => {
|
||
let lat = document.querySelector("input[name=\"cp\[lattitude\]\"").value
|
||
let lon = document.querySelector("input[name=\"cp\[longitude\]\"").value
|
||
map.setView(new L.LatLng(parseFloat(lat), parseFloat(lon)), 16)
|
||
}
|
||
}
|
||
})
|
||
|
||
document.querySelector("#map_controls").append(panToCenter)
|
||
}
|
||
}
|
||
|
||
function makeCurrentPointHigherOnMap() {
|
||
map.eachLayer(function (layer) {
|
||
if (layer instanceof L.Marker) {
|
||
if (layer.getElement().classList.contains("leaflet-marker-draggable")) {
|
||
layer.setZIndexOffset(9999)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function addRoutesList() {
|
||
(async () => {
|
||
let routes = await getRoutes()
|
||
|
||
if (routes.error || routes.length == 0) return
|
||
|
||
let result = Tag.div()
|
||
result.append("Маршруты: ")
|
||
for (const [i, route] of routes.entries()) {
|
||
if (i !== 0) {
|
||
result.append(", ")
|
||
}
|
||
|
||
result.append(Tag.a({
|
||
href: `/${getCompetition()}/` + catLinks["Редактор маршрута"](route.cat_id),
|
||
textContent: route.name
|
||
}))
|
||
}
|
||
|
||
document.querySelector(`#map_controls`).after(result)
|
||
})()
|
||
}
|
||
|
||
function checkIfAlwaysPrettify(form, oldTable, container, insertedFileRows, removedFilesLinks) {
|
||
if (localStorage.getItem(localStorageItems.NOT_PRETTIFY_EDIT_CP) === null) {
|
||
document.querySelector(`input[name^="always-prettify-0"]`).click()
|
||
prettifyEditCpForm(form, oldTable, container, insertedFileRows, removedFilesLinks)
|
||
}
|
||
}
|
||
|
||
function addPrettifyEditPageCss() {
|
||
addStylesToHead(`
|
||
#content-wrapper {
|
||
display: flex;
|
||
gap: 20px;
|
||
|
||
#props {
|
||
width: unset !important;
|
||
}
|
||
|
||
form {
|
||
width: 50%;
|
||
}
|
||
|
||
#map-wrapper {
|
||
width: 50%;
|
||
}
|
||
|
||
#map {
|
||
width: unset !important;
|
||
}
|
||
|
||
#new {
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
& > :not(:first-child) {
|
||
padding-top: 5px;
|
||
border-top: 1px solid #ddd;
|
||
}
|
||
|
||
& > :not(:last-child) {
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.map-icon {
|
||
width: 15px;
|
||
height: 15px;
|
||
}
|
||
|
||
.copy-button {
|
||
all: unset;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.copy-button img {
|
||
width: 15px;
|
||
heigth: 15px;
|
||
}
|
||
|
||
input[type=radio], input[type=checkbox] {
|
||
margin: 0;
|
||
}
|
||
|
||
input[type=radio] {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
input[type="file"] {
|
||
display: none;
|
||
}
|
||
|
||
div > input[type="checkbox"] {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
input#cp_number {
|
||
width: 3em;
|
||
}
|
||
|
||
select#cps_main {
|
||
width: 6em;
|
||
}
|
||
|
||
input:is(#lat, #lng) {
|
||
width: 5.5em;
|
||
}
|
||
|
||
input[name="cp[name_int]"] {
|
||
width: 34em;
|
||
}
|
||
}
|
||
|
||
#cps_main {
|
||
width: 150px;
|
||
}
|
||
|
||
input[type=submit] {
|
||
width: fit-content;
|
||
}
|
||
|
||
.desc-icon-container {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.desc-icon {
|
||
width: 15px;
|
||
height: 15px;
|
||
cursor: help;
|
||
}
|
||
|
||
button, input[type=submit] {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.buttons-row__content-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.custom-file-upload {
|
||
display: block;
|
||
width: fit-content;
|
||
border: 1px solid black;
|
||
padding: 5px 14px;
|
||
border-radius: 5px;
|
||
box-shadow: 0 1px 3px #ddd;
|
||
background: linear-gradient(white, #ddd);
|
||
}
|
||
|
||
.buttons > * > * {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.always-prettify-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.header > * {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
:is(.header, .options) input {
|
||
width: unset;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
gap: 30px;
|
||
align-items: start;
|
||
}
|
||
|
||
.options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.options > * {
|
||
display: flex;
|
||
gap: 5px;
|
||
align-items: center;
|
||
}
|
||
|
||
.legend-container {
|
||
display: flex;
|
||
}
|
||
|
||
.legend-container > :not(:last-child) {
|
||
padding-right: 15px;
|
||
border-right: 1px solid #ddd;
|
||
}
|
||
|
||
.legend-container > :not(:first-child) {
|
||
padding-left: 15px;
|
||
}
|
||
|
||
.legend-container > * {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 0 1 48%;
|
||
gap: 5px;
|
||
}
|
||
|
||
.legend-container__desc-header {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
justify-content: end;
|
||
}
|
||
|
||
.legend-desc {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.collapsed, .hidden {
|
||
display: none;
|
||
}
|
||
|
||
.collapse-button::after {
|
||
content: "Свернуть"
|
||
}
|
||
|
||
.collapsed + * > .collapse-button::after {
|
||
content: "Развернуть"
|
||
}
|
||
|
||
:is(.comment, .legend-desc) > * {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
:is(.comment, .legend-desc) label {
|
||
display: block;
|
||
width: 10em;
|
||
}
|
||
|
||
:is(.comment, .legend-desc) > * > :nth-child(2) {
|
||
flex: 1 1 auto;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.files-container input[type=radio][value="c"] {
|
||
display: none;
|
||
}
|
||
|
||
.file-list-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.file-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.file-container img.preview {
|
||
width: 120px;
|
||
}
|
||
|
||
.file-container img.preview-small {
|
||
width: 60px;
|
||
}
|
||
|
||
.file-container img:is(.preview, .preview-small) {
|
||
cursor: zoom-in;
|
||
}
|
||
|
||
dialog .preview,
|
||
.swiper {
|
||
max-width: 1000px;
|
||
width: 100%;
|
||
max-height: 500px;
|
||
height: 100%;
|
||
}
|
||
|
||
.swiper-button-next,
|
||
.swiper-button-prev {
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
}
|
||
|
||
.swiper-wrapper {
|
||
align-items: center;
|
||
}
|
||
|
||
.swiper-wrapper > .swiper-slide {
|
||
width: 100%;
|
||
max-height: 500px;
|
||
height: 100%;
|
||
}
|
||
|
||
.swiper-slide img {
|
||
width: 100%;
|
||
max-height: 500px;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
object-position: center;
|
||
}
|
||
|
||
.swiper-download-link {
|
||
display: block;
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
background: white;
|
||
padding: 5px 10px;
|
||
margin: 5px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 7.5px;
|
||
}
|
||
|
||
button.button-delete {
|
||
padding: 0;
|
||
border: 0;
|
||
margin: 0;
|
||
background: none;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.admin-files-container {
|
||
display: flex;
|
||
}
|
||
|
||
.admin-files-container > :not(:last-child) {
|
||
padding-right: 10px;
|
||
border-right: 1px solid #ddd;
|
||
}
|
||
|
||
.admin-files-container > :not(:first-child) {
|
||
padding-left: 10px;
|
||
}
|
||
|
||
dialog img.preview {
|
||
width: auto;
|
||
}
|
||
|
||
.file-buttons-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
`)
|
||
}
|
||
|
||
function prettifyEditCpPage() {
|
||
addCss("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css")
|
||
addCss("https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css")
|
||
addPrettifyEditPageCss()
|
||
|
||
/* NEW DEFALUT VALUES */
|
||
|
||
chooseLegendAsDefaultVariant()
|
||
|
||
/* CONTAINER */
|
||
|
||
let form = document.querySelector('form')
|
||
let rows = getRows()
|
||
let insertedFileRows = getInsertedFileRows(rows)
|
||
let removedFilesLinks = []
|
||
|
||
let container = makeContainer()
|
||
let oldTable = document.querySelector("#props")
|
||
|
||
/* HEADER */
|
||
|
||
let headerContainer = makeHeader(rows)
|
||
|
||
/* TOP BUTTONS */
|
||
|
||
let [topButtonsContainer, bottomButtonsContainer] = makeTopAndBottomButtons(rows, form, oldTable, container, insertedFileRows, removedFilesLinks)
|
||
|
||
addUglyDeleteListener()
|
||
createSendButtons()
|
||
|
||
/* LEGEND */
|
||
|
||
let legendContainer = makeLegend(rows)
|
||
legendContainer.append(makeLegendFiles(rows))
|
||
|
||
/* APPEND ALL */
|
||
|
||
container.append(topButtonsContainer)
|
||
container.append(headerContainer)
|
||
container.append(makeTopOptions(rows))
|
||
container.append(makeComment(rows))
|
||
container.append(legendContainer)
|
||
container.append(makeAdminFiles(rows))
|
||
container.append(makeBottomOptions(rows))
|
||
container.append(bottomButtonsContainer)
|
||
|
||
/* MAP */
|
||
|
||
formatMap()
|
||
makeCurrentPointHigherOnMap()
|
||
addRoutesList()
|
||
|
||
/* DIALOG */
|
||
|
||
makeDialog()
|
||
|
||
/* PRETTIFY CHECKBOX */
|
||
|
||
checkIfAlwaysPrettify(form, oldTable, container, insertedFileRows, removedFilesLinks)
|
||
}
|
||
|
||
function isAllPointsSouthOfLat(lat) {
|
||
map.eachLayer(function (layer) {
|
||
if (layer instanceof L.Marker) {
|
||
let latLng = layer.getLatLng()
|
||
if (latLng.lat > lat) {
|
||
return false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function centerMap() {
|
||
if (map !== undefined && L !== undefined) {
|
||
let coords = mapsCenterByCompetition[getCompetition()]
|
||
if (coords == null) return
|
||
|
||
if (isAllPointsSouthOfLat(lat)) {
|
||
map.setView(new L.LatLng(coords.ifSouthern.lat, coords.ifSouthern.lon), 13)
|
||
}
|
||
else {
|
||
map.setView(new L.LatLng(coords.ifEverywhere.lat, coords.ifEverywhere.lon), 12)
|
||
}
|
||
}
|
||
}
|
||
|
||
function prettifyRouteBuildPage() {
|
||
function changeColumnWidth() {
|
||
let styles = `
|
||
#content table table {
|
||
td:nth-child(2) {
|
||
width: 10%;
|
||
|
||
select {
|
||
width: 5em !important;
|
||
}
|
||
}
|
||
|
||
td:nth-child(3) {
|
||
width: 12%;
|
||
}
|
||
|
||
td:nth-child(4) {
|
||
width: 12%;
|
||
|
||
input {
|
||
width: 4em;
|
||
}
|
||
}
|
||
}
|
||
`
|
||
addStylesToHead(styles)
|
||
}
|
||
|
||
function hideDescription() {
|
||
document.querySelectorAll(`#content > form > table > tbody > tr:is(:nth-child(3), :nth-child(6)) `).forEach(el => el.remove())
|
||
}
|
||
|
||
function hideStartRow() {
|
||
document.querySelector(`#content table table tr:nth-child(2) `).remove()
|
||
}
|
||
|
||
function getRows() {
|
||
return [...document.querySelectorAll(`#content > form > table > tbody > tr:nth-child(3) tr`)]
|
||
}
|
||
|
||
function useColspanForFinishWarning() {
|
||
let rows = getRows()
|
||
let finishRow = rows.find(el => el.querySelector(`td`)?.textContent.trim() == "Финиш маршрута")
|
||
|
||
finishRow.querySelectorAll(`td:not(:nth-child(2))`).forEach(el => el.remove())
|
||
finishRow.querySelector(`td`).colSpan = 4
|
||
}
|
||
|
||
function makeStatsSpan(text) {
|
||
let statsSpan = Tag.span({
|
||
textContent: text,
|
||
classes: "stats"
|
||
})
|
||
|
||
return statsSpan
|
||
}
|
||
|
||
function getPointCells() {
|
||
return [...document.querySelectorAll(`#props table td:first-child`)]
|
||
}
|
||
|
||
function getPointNumberFromCell(cell) {
|
||
return cell.textContent.trim().match(/^\d+/)?.[0]
|
||
}
|
||
|
||
function getPointNumbers() {
|
||
let pointNumbers = {}
|
||
for (const [rowIndex, pointCell] of getPointCells().entries()) {
|
||
let pointNumber = getPointNumberFromCell(pointCell)
|
||
if (pointNumber != null)
|
||
pointNumbers[rowIndex] = pointNumber
|
||
}
|
||
|
||
return pointNumbers
|
||
}
|
||
|
||
async function getPointsFromRows() {
|
||
return await getPointsByField("number", Object.values(getPointNumbers()))
|
||
}
|
||
|
||
function countCp(points) {
|
||
let rows = getRows()
|
||
|
||
let fullCount = 0
|
||
let stageCount = 0
|
||
let fullPuzzleCount = 0
|
||
let stagePuzzleCount = 0
|
||
let fullNeedHistoryCount = 0
|
||
let stageNeedHistoryCount = 0
|
||
let fullHistoryCount = 0
|
||
let stageHistoryCount = 0
|
||
|
||
let stageRow = null
|
||
for (const [i, row] of rows.entries()) {
|
||
if (i < 2) continue
|
||
let isFinish = row.querySelector(".attention") !== null
|
||
|
||
if (isFinish || row.querySelector(`th`)) {
|
||
if (stageRow) {
|
||
let stageHeader = stageRow.querySelector("th")
|
||
stageHeader.append(makeStatsSpan(`КП в этапе: ${stageCount}`))
|
||
stageHeader.append(makeStatsSpan(`Загадок: ${stagePuzzleCount}`))
|
||
stageHeader.append(makeStatsSpan(`Нужно ИС: ${stageNeedHistoryCount}`))
|
||
stageHeader.append(makeStatsSpan(`ИС: ${stageHistoryCount}`))
|
||
}
|
||
|
||
if (isFinish) break
|
||
|
||
stageRow = row
|
||
stageCount = 0
|
||
stagePuzzleCount = 0
|
||
stageNeedHistoryCount = 0
|
||
stageHistoryCount = 0
|
||
continue
|
||
}
|
||
|
||
let pointNumber = getPointNumberFromCell(row.querySelector("td"))
|
||
if (!pointNumber) continue
|
||
|
||
fullCount++
|
||
stageCount++
|
||
|
||
let point = points[pointNumber]
|
||
if (!point) continue
|
||
|
||
if (point.is_puzzle) {
|
||
fullPuzzleCount++
|
||
stagePuzzleCount++
|
||
}
|
||
|
||
if (point.history_needed) {
|
||
fullNeedHistoryCount++
|
||
stageNeedHistoryCount++
|
||
}
|
||
|
||
if (point.history_ru) {
|
||
fullHistoryCount++
|
||
stageHistoryCount++
|
||
}
|
||
}
|
||
|
||
let tableHeader = document.querySelector("table tr:nth-child(2) th")
|
||
tableHeader.append(makeStatsSpan(`Всего КП: ${fullCount}`))
|
||
tableHeader.append(makeStatsSpan(`Загадок: ${fullPuzzleCount}`))
|
||
tableHeader.append(makeStatsSpan(`Нужно ИС: ${fullNeedHistoryCount}`))
|
||
tableHeader.append(makeStatsSpan(`ИС: ${fullHistoryCount}`))
|
||
|
||
addStylesToHead(`
|
||
.stats {
|
||
color: red;
|
||
margin-left: .5em;
|
||
}
|
||
`)
|
||
}
|
||
|
||
function addLinksToCp(points) {
|
||
let pointCells = getPointCells()
|
||
|
||
for (const [i, pointCell] of pointCells.entries()) {
|
||
let pointNumber = getPointNumberFromCell(pointCell)
|
||
if (pointNumber == null) continue
|
||
|
||
let cellContent = pointCell.textContent
|
||
pointCell.innerHTML = ''
|
||
pointCell.append(Tag.a({
|
||
href: cpLink(points[pointNumber].cp_id),
|
||
target: "_blank",
|
||
textContent: cellContent
|
||
}))
|
||
}
|
||
}
|
||
|
||
function showHistoryData(points) {
|
||
let pointCells = getPointCells()
|
||
|
||
for (const [i, pointCell] of pointCells.entries()) {
|
||
let pointNumber = getPointNumberFromCell(pointCell)
|
||
if (pointNumber == null) continue
|
||
|
||
let point = points[pointNumber]
|
||
|
||
if (point.history_needed) {
|
||
pointCell.append(Tag.span({ classes: "point-info", textContent: "Нужна ИС" }))
|
||
}
|
||
|
||
if (point.history_ru) {
|
||
pointCell.append(Tag.span({ classes: "point-info", textContent: "Есть ИС" }))
|
||
}
|
||
}
|
||
|
||
addStylesToHead(`
|
||
.point-info {
|
||
&:first-of-type {
|
||
margin-left: 5px;
|
||
}
|
||
|
||
& + &::before {
|
||
content: " | "
|
||
}
|
||
}
|
||
`)
|
||
}
|
||
|
||
function getRouteName() {
|
||
return document.querySelector(`#content h1`).textContent.match(/(?<=\")[^\"]+/)
|
||
}
|
||
|
||
function sendRouteData() {
|
||
createPseudoButton(document.querySelector(`input[name="save_sort"]`), async () => {
|
||
let formData = new FormData(document.querySelector(`form[name="main_form"]`))
|
||
|
||
let catId = urlParams().get("cat_id")
|
||
formData.set("cat_id", catId)
|
||
formData.set("cat_name", getRouteName())
|
||
await updateRoute(formData)
|
||
})
|
||
}
|
||
|
||
function showUnknownPoints() {
|
||
(async () => {
|
||
let unknownPoints = await getUnknownPoints()
|
||
if (unknownPoints.error || !unknownPoints.length) return
|
||
|
||
let unknownPointsContainer = Tag.div()
|
||
unknownPointsContainer.append("Несохраненные точки: ")
|
||
for (const [i, cpId] of unknownPoints.entries()) {
|
||
if (i !== 0) {
|
||
unknownPointsContainer.append(", ")
|
||
}
|
||
|
||
unknownPointsContainer.append(Tag.a({
|
||
href: cpLink(cpId),
|
||
textContent: cpId
|
||
}))
|
||
}
|
||
document.querySelector(`#content h1`).after(unknownPointsContainer)
|
||
})()
|
||
}
|
||
|
||
addJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js")
|
||
changeColumnWidth()
|
||
hideDescription()
|
||
hideStartRow()
|
||
useColspanForFinishWarning()
|
||
;(async () => {
|
||
let points = await getPointsFromRows()
|
||
countCp(points)
|
||
addLinksToCp(points)
|
||
showHistoryData(points)
|
||
})()
|
||
sendRouteData()
|
||
showUnknownPoints()
|
||
}
|
||
|
||
function getCpNumberFromOption(option) {
|
||
return option.textContent.match(/(?<=^\s*#)\d+/g)?.[0]
|
||
}
|
||
|
||
function matchNumberFromSelect(select) {
|
||
return [...document.querySelector(select).options].reduce(
|
||
(res, el) => ({ ...res, [getCpNumberFromOption(el)]: el.value }),
|
||
{}
|
||
)
|
||
}
|
||
|
||
function unselectAll(select) {
|
||
[...document.querySelector(select).selectedOptions].forEach(el => el.selected = false)
|
||
}
|
||
|
||
function scrollSelectToBottom(select) {
|
||
document.querySelector(`${select} option:last-child`).scrollIntoView()
|
||
}
|
||
|
||
function prettifyRouteEditPage() {
|
||
function createPointsInpit() {
|
||
let pointInput = Tag.input({
|
||
id: "add-point",
|
||
type: "text",
|
||
on: {
|
||
focus: () => {
|
||
pointInput.classList.remove("success", "error")
|
||
}
|
||
}
|
||
})
|
||
|
||
let addButton = Tag.button({
|
||
type: "button",
|
||
textContent: "Добавить",
|
||
id: "add-button",
|
||
on: {
|
||
click: () => {
|
||
let point = pointInput.value
|
||
if (point == "") return
|
||
|
||
let availablePoints = matchNumberFromSelect("#cps_avail")
|
||
let addedPoints = matchNumberFromSelect("#cps_in")
|
||
|
||
if (point in addedPoints) {
|
||
alert("Точка уже добавлена")
|
||
return
|
||
}
|
||
|
||
if (!(point in availablePoints)) {
|
||
alert("Точка не найдена")
|
||
return
|
||
}
|
||
|
||
unselectAll("#cps_avail")
|
||
let option = document.querySelector(`#cps_avail option[value="${availablePoints[point]}"]`)
|
||
option.selected = true
|
||
document.querySelector(`#cps_add`).click()
|
||
pointInput.classList.add("success")
|
||
pointInput.value = ""
|
||
option.selected = false
|
||
scrollSelectToBottom("#cps_in")
|
||
}
|
||
}
|
||
})
|
||
|
||
let removeButton = Tag.button({
|
||
type: "button",
|
||
textContent: "Убрать",
|
||
on: {
|
||
click: () => {
|
||
let point = pointInput.value
|
||
if (point == "") return
|
||
|
||
let addedPoints = matchNumberFromSelect("#cps_in")
|
||
|
||
if (!(point in addedPoints)) return
|
||
|
||
unselectAll("#cps_in")
|
||
let option = document.querySelector(`#cps_in option[value="${addedPoints[point]}"]`)
|
||
option.selected = true
|
||
document.querySelector(`#cps_delete`).click()
|
||
pointInput.classList.add("error")
|
||
pointInput.value = ""
|
||
option.selected = false
|
||
scrollSelectToBottom("#cps_in")
|
||
}
|
||
}
|
||
})
|
||
|
||
let addPointsContainer = Tag.div({
|
||
children: [
|
||
addButton,
|
||
pointInput,
|
||
removeButton
|
||
]
|
||
})
|
||
document.querySelector("table tr:nth-child(16) td").prepend(addPointsContainer)
|
||
|
||
addStylesToHead(`
|
||
#cps_in, #cps_avail {
|
||
height: 15em;
|
||
}
|
||
|
||
.success {
|
||
border-color: green;
|
||
}
|
||
|
||
.error {
|
||
border-color: red;
|
||
}
|
||
|
||
#add-point {
|
||
width: 5em;
|
||
}
|
||
|
||
#add-button {
|
||
margin-left: 420px;
|
||
}
|
||
`)
|
||
}
|
||
|
||
function makeTextareasOneRow() {
|
||
document.querySelectorAll(`textarea:is([name="track[comment_int]"], [name="track[comment_ext]"])`).forEach(el => {
|
||
el.rows = 1
|
||
})
|
||
}
|
||
|
||
function hideRedundantRows() {
|
||
document.querySelectorAll(`table tr:is(:nth-child(3), :nth-child(4), :nth-child(5))`).forEach(el => el.classList.add("hidden"))
|
||
}
|
||
|
||
async function getPointsFromSelect() {
|
||
let options = [...document.querySelectorAll(`#cps_in option`)]
|
||
return await getPointsByField("number", options.map(option => getCpNumberFromOption(option)))
|
||
}
|
||
|
||
function paintCpList(points) {
|
||
let options = [...document.querySelectorAll(`#cps_in option`)]
|
||
let catId = urlParams().get("cat_id")
|
||
|
||
for (const option of options) {
|
||
let category = points[getCpNumberFromOption(option)].categories.find(category => category.cat_id === catId)
|
||
if (category && category.pivot.bonus_time == "0") {
|
||
let stage = parseInt(category.pivot.stage)
|
||
option.style['background-color'] = stageColors[(stage - 1) % stageColors.length]
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideAlreadyAddedPoints(points) {
|
||
let options = [...document.querySelectorAll(`#cps_avail option`)]
|
||
|
||
for (const option of options) {
|
||
if (getCpNumberFromOption(option) in points) {
|
||
option.style.display = "none"
|
||
}
|
||
}
|
||
}
|
||
|
||
makeTextareasOneRow()
|
||
hideRedundantRows()
|
||
createPointsInpit()
|
||
;(async () => {
|
||
let points = await getPointsFromSelect()
|
||
paintCpList(points)
|
||
hideAlreadyAddedPoints(points)
|
||
})()
|
||
}
|
||
|
||
function toggleStagePageRows(rows) {
|
||
let showIndex = [0, 3]
|
||
let to = rows.findIndex(el => el.querySelector("th")?.textContent.trim() == "Бонусы")
|
||
if (to == -1)
|
||
to = rows.length
|
||
|
||
toggleRows(rows, 3, to, 8, showIndex)
|
||
toggleRows(rows, to, rows.length, 1, [])
|
||
}
|
||
|
||
function hideUselessRowsFromRouteStagesPage() {
|
||
let styles = `
|
||
tr.collapsed {
|
||
display: block;
|
||
width: 0;
|
||
height: 0;
|
||
overflow: hidden;
|
||
}
|
||
`
|
||
|
||
let rows = [...document.querySelectorAll(`#content tbody tr`)]
|
||
let collapseButton = Tag.button({
|
||
type: "button",
|
||
textContent: "Показать",
|
||
on: {
|
||
click: () => {
|
||
toggleStagePageRows(rows)
|
||
collapseButton.textContent = toggleText(collapseButton.textContent, "Показать", "Скрыть")
|
||
}
|
||
}
|
||
})
|
||
|
||
toggleStagePageRows(rows)
|
||
|
||
document.querySelector(`table tr:first-child th`).append(collapseButton)
|
||
|
||
addStylesToHead(styles)
|
||
}
|
||
|
||
function addCpNameToOptions() {
|
||
let options = [...document.querySelectorAll(`option`)]
|
||
let cpIds = [...new Set(
|
||
options
|
||
.map($option => $option.value)
|
||
.filter($value => $value != null)
|
||
)
|
||
]
|
||
|
||
;(async () => {
|
||
let points = await getPointsByField("cp_id", cpIds)
|
||
|
||
for (const option of options) {
|
||
if (option.value != null && points[option.value] != null) {
|
||
option.innerText = option.innerText + " - " + points[option.value].name_int
|
||
}
|
||
}
|
||
})()
|
||
}
|
||
|
||
function addCommonStyles() {
|
||
addStylesToHead(`
|
||
#content h1 {
|
||
margin-bottom: 0;
|
||
}
|
||
`)
|
||
}
|
||
|
||
function getDisabledPages() {
|
||
return JSON.parse(localStorage.getItem(localStorageItems.DISABLED_PAGES) ?? "{}")
|
||
}
|
||
|
||
function saveDisabledPages(pages) {
|
||
localStorage.setItem(localStorageItems.DISABLED_PAGES, JSON.stringify(pages))
|
||
}
|
||
|
||
function isPageDisabled() {
|
||
let disabledPages = getDisabledPages()
|
||
let pageType = getPageType()
|
||
|
||
return disabledPages[pageType]?.[getAction()]
|
||
}
|
||
|
||
function addDisabledPage() {
|
||
let disabledPages = getDisabledPages()
|
||
let pageType = getPageType()
|
||
if (!(pageType in disabledPages)) {
|
||
disabledPages[pageType] = {}
|
||
}
|
||
disabledPages[pageType][getAction()] = true
|
||
|
||
saveDisabledPages(disabledPages)
|
||
}
|
||
|
||
function removeDisabledPage() {
|
||
let disabledPages = getDisabledPages()
|
||
let pageType = getPageType()
|
||
if (!(pageType in disabledPages)) {
|
||
return
|
||
}
|
||
delete disabledPages[pageType][getAction()]
|
||
|
||
saveDisabledPages(disabledPages)
|
||
}
|
||
|
||
function addEnableButtons() {
|
||
let enableButton = Tag.button({
|
||
type: "button",
|
||
id: "enable-button",
|
||
textContent: "Сделать красиво",
|
||
on: {
|
||
click: () => {
|
||
if (pretty) return
|
||
|
||
hide([enableButton])
|
||
prettify()
|
||
}
|
||
}
|
||
})
|
||
|
||
if (!isPageDisabled()) {
|
||
hide([enableButton])
|
||
}
|
||
|
||
let header = document.querySelector("#header")
|
||
header.insertBefore(enableButton, header.querySelector("#globalmenu"))
|
||
header.insertBefore(createAlwaysEnable(), header.querySelector("#globalmenu"))
|
||
|
||
addStylesToHead(`
|
||
#always-enable-container {
|
||
display: inline-block;
|
||
}
|
||
|
||
.hidden {
|
||
display: none;
|
||
}
|
||
`)
|
||
}
|
||
|
||
function addDisableButton() {
|
||
let disableButton = Tag.button({
|
||
id: "disable-button",
|
||
type: "button",
|
||
textContent: "Сделать как было",
|
||
on: {
|
||
click: () => {
|
||
if (!pretty) return
|
||
|
||
if (!confirm("Это действие отключит скрипт на этой странице и удалит все несохраненные данные")) return
|
||
|
||
addDisabledPage()
|
||
pretty = false
|
||
location.reload()
|
||
}
|
||
}
|
||
})
|
||
|
||
header.querySelector("#enable-button").after(disableButton)
|
||
}
|
||
|
||
function createAlwaysEnable() {
|
||
let alwaysEnableContainer = Tag.div({
|
||
id: "always-enable-container",
|
||
children: [
|
||
Tag.make("label", {
|
||
_for: "always-enable",
|
||
textContent: "Всегда красиво"
|
||
})
|
||
]
|
||
})
|
||
|
||
let alwaysEnableCheckbox = Tag.input({
|
||
type: "checkbox",
|
||
id: "always-enable",
|
||
name: "always-enable",
|
||
on: {
|
||
change: function () {
|
||
if (this.checked)
|
||
removeDisabledPage()
|
||
else
|
||
addDisabledPage()
|
||
}
|
||
}
|
||
})
|
||
|
||
|
||
if (!isPageDisabled()) {
|
||
alwaysEnableCheckbox.checked = true
|
||
}
|
||
|
||
alwaysEnableContainer.append(alwaysEnableCheckbox)
|
||
|
||
return alwaysEnableContainer
|
||
}
|
||
|
||
function prettify() {
|
||
if (pretty) return
|
||
|
||
pretty = true
|
||
|
||
addDisableButton()
|
||
|
||
addCommonStyles()
|
||
|
||
/* SWITCH FOR DIFFERENT PAGES */
|
||
|
||
addStickyMenu()
|
||
|
||
if (isCpEditPage()) {
|
||
prettifyEditCpPage()
|
||
}
|
||
|
||
if (isRouteListPage()) {
|
||
addStageLink()
|
||
hideNonTesters()
|
||
}
|
||
|
||
if (isRouteBuildPage()) {
|
||
prettifyRouteBuildPage()
|
||
}
|
||
|
||
if (isRouteEditPage()) {
|
||
prettifyRouteEditPage()
|
||
}
|
||
|
||
if (isRouteStagesPage()) {
|
||
hideUselessRowsFromRouteStagesPage()
|
||
addCpNameToOptions()
|
||
}
|
||
}
|
||
|
||
function prettifyIfEnabled() {
|
||
if (isPageDisabled()) return
|
||
|
||
prettify()
|
||
}
|
||
|
||
/* REDIRECTS */
|
||
|
||
redirectAfterNewCpIfNeeded()
|
||
|
||
/* PRETTIFY */
|
||
|
||
let pretty = false
|
||
|
||
addEnableButtons()
|
||
|
||
if (isCpDeletePage()) {
|
||
bindDeleteButton()
|
||
return
|
||
}
|
||
|
||
prettifyIfEnabled()
|
||
|
||
/* MAPS */
|
||
|
||
addClearBoth()
|
||
|
||
if (hasMap()) {
|
||
initMapbox()
|
||
addFullscreenButton()
|
||
|
||
if (isRouteBuildPage() || isRouteMapPage()) {
|
||
centerMap()
|
||
}
|
||
}
|
||
})(); |