tampermonkey/main.js
2025-03-18 10:11:44 +00:00

1937 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==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 links = {
"Волонтеры": "suv_comp",
"Контрольные пункты": "cp_mgmt",
"Добавить новый КП": "cp_mgmt/?action=edit",
"Маршруты": "route_mgmt",
}
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",
ALWAYS_PRETTIFY: "always",
REDIRECT_EXIT: "redirectExit"
}
const ZOOM = 17
const METERS = 510
let removedFilesLinks = []
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 div = document.createElement('div')
let desc = altDesc ?? this.desc
desc = desc ? "<div class='desc-icon-container' title='" + desc + "'>" +
"<img class='desc-icon' src='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnIGlkPSJzdmcyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAwIiB3aWR0aD0iMjAwIiB2ZXJzaW9uPSIxLjAiPgogPHBhdGggaWQ9InBhdGgyMzgyIiBkPSJtMTY1LjMzIDExMy40NGExMDMuNjEgMTAzLjYxIDAgMSAxIC0yMDcuMjIgMCAxMDMuNjEgMTAzLjYxIDAgMSAxIDIwNy4yMiAweiIgdHJhbnNmb3JtPSJtYXRyaXgoLjkzNzM5IDAgMCAuOTM3MzkgNDIuMTQzIC02LjMzOTIpIiBzdHJva2Utd2lkdGg9IjAiIGZpbGw9IiNmZmYiLz4KIDxnIGlkPSJsYXllcjEiPgogIDxwYXRoIGlkPSJwYXRoMjQxMyIgZD0ibTEwMCAwYy01NS4yIDAtMTAwIDQ0LjgtMTAwIDEwMC01LjA0OTVlLTE1IDU1LjIgNDQuOCAxMDAgMTAwIDEwMHMxMDAtNDQuOCAxMDAtMTAwLTQ0LjgtMTAwLTEwMC0xMDB6bTAgMTIuODEyYzQ4LjEzIDAgODcuMTkgMzkuMDU4IDg3LjE5IDg3LjE4OHMtMzkuMDYgODcuMTktODcuMTkgODcuMTktODcuMTg4LTM5LjA2LTg3LjE4OC04Ny4xOSAzOS4wNTgtODcuMTg4IDg3LjE4OC04Ny4xODh6bTEuNDcgMjEuMjVjLTUuNDUgMC4wMy0xMC42NTMgMC43MzctMTUuMjgyIDIuMDYzLTQuNjk5IDEuMzQ2LTkuMTI2IDMuNDg0LTEyLjg3NiA2LjIxOS0zLjIzOCAyLjM2Mi02LjMzMyA1LjM5MS04LjY4NyA4LjUzMS00LjE1OSA1LjU0OS02LjQ2MSAxMS42NTEtNy4wNjMgMTguNjg3LTAuMDQgMC40NjgtMC4wNyAwLjg2OC0wLjA2MiAwLjg3NiAwLjAxNiAwLjAxNiAyMS43MDIgMi42ODcgMjEuODEyIDIuNjg3IDAuMDUzIDAgMC4xMTMtMC4yMzQgMC4yODItMC45MzcgMS45NDEtOC4wODUgNS40ODYtMTMuNTIxIDEwLjk2OC0xNi44MTMgNC4zMi0yLjU5NCA5LjgwOC0zLjYxMiAxNS43NzgtMi45NjkgMi43NCAwLjI5NSA1LjIxIDAuOTYgNy4zOCAyIDIuNzEgMS4zMDEgNS4xOCAzLjM2MSA2Ljk0IDUuODEzIDEuNTQgMi4xNTYgMi40NiA0LjU4NCAyLjc1IDcuMzEyIDAuMDggMC43NTkgMC4wNSAyLjQ4LTAuMDMgMy4yMTktMC4yMyAxLjgyNi0wLjcgMy4zNzgtMS41IDQuOTY5LTAuODEgMS41OTctMS40OCAyLjUxNC0yLjc2IDMuODEyLTIuMDMgMi4wNzctNS4xOCA0LjgyOS0xMC43OCA5LjQwNy0zLjYgMi45NDQtNi4wNCA1LjE1Ni04LjEyIDcuMzQzLTQuOTQzIDUuMTc5LTcuMTkxIDkuMDY5LTguNTY0IDE0LjcxOS0wLjkwNSAzLjcyLTEuMjU2IDcuNTUtMS4xNTYgMTMuMTkgMC4wMjUgMS40IDAuMDYyIDIuNzMgMC4wNjIgMi45N3YwLjQzaDIxLjU5OGwwLjAzLTIuNGMwLjAzLTMuMjcgMC4yMS01LjM3IDAuNTYtNy40MSAwLjU3LTMuMjcgMS40My01IDMuOTQtNy44MSAxLjYtMS44IDMuNy0zLjc2IDYuOTMtNi40NyA0Ljc3LTMuOTkxIDguMTEtNi45OSAxMS4yNi0xMC4xMjUgNC45MS00LjkwNyA3LjQ2LTguMjYgOS4yOC0xMi4xODcgMS40My0zLjA5MiAyLjIyLTYuMTY2IDIuNDYtOS41MzIgMC4wNi0wLjgxNiAwLjA3LTMuMDMgMC0zLjk2OC0wLjQ1LTcuMDQzLTMuMS0xMy4yNTMtOC4xNS0xOS4wMzItMC44LTAuOTA5LTIuNzgtMi44ODctMy43Mi0zLjcxOC00Ljk2LTQuMzk0LTEwLjY5LTcuMzUzLTE3LjU2LTkuMDk0LTQuMTktMS4wNjItOC4yMy0xLjYtMTMuMzUtMS43NS0wLjc4LTAuMDIzLTEuNTktMC4wMzYtMi4zNy0wLjAzMnptLTEwLjkwOCAxMDMuNnYyMmgyMS45OTh2LTIyaC0yMS45OTh6Ii8+CiA8L2c+Cjwvc3ZnPgo='/>" +
"</div>" : ""
div.innerHTML = "<label for=''>" + (altName ?? this.name ?? "") + "</label>" + "<div>" + (this.content ?? "") + "</div>" +
desc
return div
}
}
function createFrom(rows, classList, data) {
let container = document.createElement('div')
container.classList.add(...classList.split(" "))
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.appendChild(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) {
let css = document.createElement("link")
css.href = link
css.rel = "stylesheet"
document.head.appendChild(css)
}
function addJs(link) {
let script = document.createElement("script")
script.src = link
document.head.appendChild(script)
}
function addStylesToHead(styles) {
let styleSheet = document.createElement("style")
styleSheet.textContent = styles
document.head.appendChild(styleSheet)
}
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 isRouteStagesPage() {
return isRouteManagement() && getAction() === "stages"
}
function isRouteMapPage() {
return isRouteManagement() && getAction() === "map"
}
async function updatePoint(formData) {
return await fetch(`https://runcity.geo.rictum.ru/api/competitions/${getCompetition()}/update`, {
method: 'POST',
body: formData
})
}
async function deletePoint() {
return await fetch(`https://runcity.geo.rictum.ru/api/points/${urlParams().get("cp_id")}`, {
method: 'DELETE'
})
}
function copyCoordinates() {
let copyButton = document.createElement("button")
copyButton.addEventListener("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])
})
copyButton.classList.add("copy-button")
let copyImage = document.createElement("img")
copyImage.src = "https://upload.wikimedia.org/wikipedia/commons/a/aa/Bw_copy_icon_320x320.svg"
copyButton.appendChild(copyImage)
return copyButton
}
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) {
let ref = document.createElement("a")
ref.href = linkCallback(lat, lon, zoom)
ref.setAttribute('target', "_blank")
ref.addEventListener("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)
})
let icon = document.createElement("img")
icon.src = iconSrc
icon.classList.add("map-icon")
ref.appendChild(icon)
return ref
}
let makeCoordinatesLinks = (function() {
var executed = false
return function () {
if (executed) return
executed = true
let linksContainer = document.createElement("div")
linksContainer.appendChild(copyCoordinates())
linksContainer.appendChild(makeRef(yandexMaps, "https://upload.wikimedia.org/wikipedia/commons/7/72/Yandex_Maps_icon.svg", ZOOM))
linksContainer.appendChild(makeRef(googleMaps, "https://upload.wikimedia.org/wikipedia/commons/a/aa/Google_Maps_icon_%282020%29.svg", METERS))
linksContainer.appendChild(makeRef(pastvu, "https://pastvu.com/coast-icon.png", ZOOM))
linksContainer.appendChild(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) {
let downloadLink = document.createElement("a")
downloadLink.setAttribute("download", name)
downloadLink.textContent = "Скачать"
downloadLink.setAttribute("target", "_blank")
if (href)
downloadLink.href = href
return downloadLink
}
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, variant = null) {
if (!inputName || !file || !removeLink) return
let fileListContainer
if (variant !== null) {
if (inputName == "attachment1" || inputName == "attachment4") {
if (variant == "l") {
let fileContainerClassName = fileInputs[inputName][variant].replace("_", "-")
fileListContainer = document.querySelector(`.${fileContainerClassName}-container .file-list-container`)
}
else if (variant == "h") {
let filesContainer = document.querySelector(inputName == "attachment1" ? `.history-photo-container` : `.history-files-container`)
fileListContainer = filesContainer.querySelector(`.file-list-container`)
if (fileListContainer == null) {
fileListContainer = document.createElement("div")
fileListContainer.classList.add("file-list-container")
filesContainer.appendChild(fileListContainer)
}
}
}
}
let input = document.querySelector(`input[name="${inputName}\[\]"]`)
fileListContainer ??= input.parentElement.parentElement.querySelector(".file-list-container")
let fileContainer = document.createElement("div")
fileContainer.classList.add("file-container")
const displayedFile = document.createElement(type ?? "a")
displayedFile.classList.add("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 = document.createElement("div")
fileButtonsContainer.classList.add("file-buttons-container")
fileButtonsContainer.appendChild(makeDownloadLink(/[^/]*$/.exec(new URL(file).pathname)[0], file))
let deleteButton = document.createElement("button")
deleteButton.type = "button"
deleteButton.classList.add("button-delete")
deleteButton.textContent = "x"
deleteButton.addEventListener("click", async e => {
if (!confirm("Точно?")) return
await fetch(removeLink)
await sendFileDeleted(inputName, variant)
removedFilesLinks.push(removeLink)
fileListContainer.removeChild(fileContainer)
})
fileButtonsContainer.appendChild(deleteButton)
fileContainer.appendChild(displayedFile)
fileContainer.appendChild(fileButtonsContainer)
fileListContainer.appendChild(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 = document.createElement(res)
if (res == "audio") {
el.setAttribute("controls", "")
}
return el
}
function getAttachmentIndex(input) {
return input.name.replace(/\D/g, '')
}
function getContainersByVariant(variant, attachmentIndex) {
let from = variant == "l" ? "history" : "legend"
let to = variant == "l" ? "legend" : "history"
let type = attachmentIndex == 1 ? "photo" : "files"
return [document.querySelector(`.${from}-${type}-container`), document.querySelector(`.${to}-${type}-container`)]
}
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 = filesContainer.querySelector(".file-list-container")
}
else {
fileListContainer = this.parentElement.parentElement.querySelector(".file-list-container")
}
let fileContainer = document.createElement("div")
fileContainer.classList.add("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 = document.createElement("div")
fileButtonsContainer.classList.add("file-buttons-container")
let downloadLink = makeDownloadLink(file.name)
fileButtonsContainer.appendChild(downloadLink)
let deleteButton = document.createElement("button")
deleteButton.classList.add("button-delete")
deleteButton.textContent = "x"
deleteButton.type = "button"
deleteButton.addEventListener("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.appendChild(deleteButton)
fileContainer.appendChild(displayedFile)
fileContainer.appendChild(fileButtonsContainer)
fileListContainer.appendChild(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() {
var executed = false
return function(insertedFileRows) {
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 = document.createElement("label")
pseudoInput.classList.add("custom-file-upload")
pseudoInput.setAttribute("for", element.id)
pseudoInput.textContent = "+"
element.parentElement.insertBefore(pseudoInput, element)
let fileListContainer = document.createElement("div")
fileListContainer.classList.add("file-list-container")
element.parentElement.parentElement.appendChild(fileListContainer)
element.addEventListener("change", makeHandleFilesFunc(), false)
})
moveFileInputValues(insertedFileRows)
}
})()
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) {
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, 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) {
to.querySelector(`.file-list-container`).appendChild(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.appendChild(became)
setInputValues(became, inputValues)
createSendButtons()
}
function prettify(form, was, became, insertedFileRows) {
moveInputValues(form, was, became)
prettifyFiles(insertedFileRows)
moveNewFilesOnVariantChange()
$("#cps_main").select2()
makeCoordinatesLinks()
}
function uglify(form, was, became) {
moveInputValues(form, was, became)
for (const removedFileLink of removedFilesLinks) {
removeDeletedFilesFromUglyVersion(removedFileLink)
}
removedFilesLinks = []
}
function createAlwaysPrettifyInput(index) {
let alwaysPrettify = document.createElement("div")
alwaysPrettify.classList.add("always-prettify-container")
let alwaysPrettifyLabel = document.createElement("label")
alwaysPrettifyLabel.setAttribute("for", "always-prettify-" + index)
alwaysPrettifyLabel.textContent = "Всегда"
alwaysPrettify.appendChild(alwaysPrettifyLabel)
let alwaysPrettifyCheckbox = document.createElement("input")
alwaysPrettifyCheckbox.type = "checkbox"
alwaysPrettifyCheckbox.id = "always-prettify-" + index
alwaysPrettifyCheckbox.name = "always-prettify-" + index
alwaysPrettifyCheckbox.addEventListener("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.setItem(localStorageItems.ALWAYS_PRETTIFY, "+")
else
localStorage.removeItem(localStorageItems.ALWAYS_PRETTIFY)
})
alwaysPrettify.appendChild(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 = 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 swiperDiv = document.createElement("div")
swiperDiv.classList.add("swiper")
let swiperWrapper = document.createElement("div")
swiperWrapper.classList.add("swiper-wrapper")
for (const file of files) {
let swiperSlide = document.createElement("div")
swiperSlide.classList.add("swiper-slide")
let downloadLink = document.createElement("a")
downloadLink.classList.add("swiper-download-link")
downloadLink.href = file.dataset.origin
downloadLink.text = "Скачать"
downloadLink.setAttribute("target", "_blank")
swiperSlide.appendChild(downloadLink)
let swiperFile = file.cloneNode(true)
if (swiperFile.dataset.origin)
swiperFile.src = swiperFile.dataset.origin
swiperSlide.appendChild(swiperFile)
swiperWrapper.appendChild(swiperSlide)
}
swiperDiv.appendChild(swiperWrapper)
let prevButton = document.createElement("div")
prevButton.classList.add("swiper-button-prev")
swiperDiv.appendChild(prevButton)
let nextButton = document.createElement("div")
nextButton.classList.add("swiper-button-next")
swiperDiv.appendChild(nextButton)
dialog.appendChild(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')
}
function bindArrowsForGallery(leftButtonQuery, rightButtonQuery) {
document.addEventListener("keydown", function(e) {
if (!document.querySelector("dialog").open) return
switch(e.key) {
case "ArrowLeft":
$(leftButtonQuery).click()
break
case "ArrowRight":
$(rightButtonQuery).click()
break
default: return
}
e.preventDefault()
})
}
async function sendForm(onSend) {
let formData = new FormData(document.querySelector("form"))
let fileListContainers = [...document.querySelectorAll(".file-list-container")]
let fileInputNames = ["legend_photo", "history_photo", "legend_files", "history_files", "admin_photo", "admin_files"]
for (let [i, fileListContainer] of fileListContainers.entries()) {
formData.set(`cp[${fileInputNames[i]}]`, [...fileListContainer.children].filter(el => !el.classList.contains("file-container-new")).length)
}
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)
}
finally {
onSend()
}
}
function createSaveAndNewButton() {
let saveAndNewButton = document.createElement("button")
saveAndNewButton.textContent = "+"
saveAndNewButton.type = "button"
saveAndNewButton.classList.add("safe-action")
saveAndNewButton.addEventListener("click", () => {
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]
for (let saveButton of saveButtons) {
let pseudoSaveButton = document.createElement("button")
pseudoSaveButton.type = "button"
pseudoSaveButton.textContent = saveButton.value
pseudoSaveButton.classList.add("safe-action", "pseudo-save")
pseudoSaveButton.addEventListener("click", async () => await sendForm(() => {
if (document.querySelector(`input[name="cp[id]"]`).value != '') {
saveButton.click()
return
}
if (saveButton.name == "save_exit") {
localStorage.setItem(localStorageItems.REDIRECT_EXIT, true)
}
saveAndStayButtons[0].click()
}))
saveButton.style.display = "none"
saveButton.parentElement.insertBefore(pseudoSaveButton, saveButton)
}
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 addStickyMenu() {
let competition = getCompetition()
if (!competition) return
let menuContainer = document.createElement("nav")
menuContainer.classList.add("sticky-menu")
let menu = document.createElement("menu")
menuContainer.appendChild(menu)
let catId = urlParams().get("cat_id")
if (catId !== null) {
for (const [label, href] of Object.entries(catLinks)) {
links[label] = href(catId)
}
}
for (const [label, href] of Object.entries(links)) {
let menuItem = document.createElement("li")
let link = document.createElement("a")
link.href = `/${competition}/${href}`
link.innerText = label
if (label === "Легенда") {
link.setAttribute("target", "_blank")
}
menuItem.appendChild(link)
menu.appendChild(menuItem)
}
document.querySelector("#header").after(menuContainer)
}
function addClearBoth() {
let clearEl = document.createElement("div")
clearEl.style.clear = "both"
document.body.appendChild(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 stagesLink = document.createElement("a")
stagesLink.innerText = "Этапы"
stagesLink.href = catLinks[stagesLink.innerText](catId)
constructorLink.after(stagesLink)
constructorLink.after(". ")
}
}
}
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() {
let container = document.createElement('div')
container.id = "new"
return container
}
function makeHeader(rows) {
let headerContainer = createFrom(rows, "header", [
{ index: 2, name: "№", desc: "" },
{ index: 8, name: "Название" }
])
let copyLink = document.createElement("div")
copyLink.innerHTML = new Property(rows[2]).desc
headerContainer.append(copyLink)
return headerContainer
}
function bindDeleteButton() {
let deleteButton = document.querySelector(`input[name="delete_go"]`)
let pseudoDeleteButton = document.createElement("button")
pseudoDeleteButton.type = "button"
pseudoDeleteButton.textContent = deleteButton.value
pseudoDeleteButton.classList.add("unsafe-action", "pseudo-save")
pseudoDeleteButton.addEventListener("click", async () => {
await deletePoint()
deleteButton.click()
})
deleteButton.style.display = "none"
deleteButton.parentElement.insertBefore(pseudoDeleteButton, deleteButton)
}
function makeTopAndBottomButtons(rows, form, oldTable, container, insertedFileRows) {
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 = document.createElement("div")
rowContentWrapper.classList.add("buttons-row__content-wrapper")
while ([...el.children].length > 0) {
let child = el.firstChild
rowContentWrapper.appendChild(child)
}
let prettifyButton = document.createElement("button")
prettifyButton.type = "button"
prettifyButton.textContent = "Сделать красиво"
prettifyButton.addEventListener("click", () => {
prettify(form, oldTable, container, insertedFileRows)
})
rowContentWrapper.appendChild(prettifyButton)
rowContentWrapper.appendChild(createAlwaysPrettifyInput(index))
el.appendChild(rowContentWrapper)
})
;[topButtonsContainer, bottomButtonsContainer].forEach((el, index) => {
let unglifyButton = document.createElement("button")
unglifyButton.type = "button"
unglifyButton.textContent = "Сделать некрасиво"
unglifyButton.addEventListener("click", () => {
uglify(form, container, oldTable)
})
let topRowContentWrapper = el.querySelector("div > div > div")
topRowContentWrapper.appendChild(unglifyButton)
topRowContentWrapper.appendChild(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 = document.createElement("div")
legendContainer.classList.add("legend-container")
/* LEGEND DESC */
let legendDescContainer = document.createElement("div")
legendDescContainer.classList.add("legend-container__desc")
let legendDescHeader = document.createElement("div")
legendDescHeader.classList.add("legend-container__desc-header")
const LEGEND_RU_LABEL = "Русский"
const LEGEND_EN_LABEL = "Английский"
let legendLang = document.createElement("div")
legendLang.classList.add("legend-desc__lang")
legendLang.textContent = LEGEND_RU_LABEL
legendDescHeader.appendChild(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.appendChild(legendEnSwitchContainer)
let copyDescButton = document.createElement("button")
copyDescButton.type = "button"
copyDescButton.textContent = "Копировать"
copyDescButton.addEventListener("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.appendChild(legendRuSwitchContainer)
legendDescHeader.appendChild(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 = document.createElement("div")
let hiderButton = document.createElement("button")
hiderButton.classList.add("collapse-button")
hiderButton.setAttribute("type", "button")
hiderButton.addEventListener("click", event => {
let container = document.querySelector('.legend-container');
container.querySelectorAll(".legend-desc.collapsible").forEach(element => {
element.classList.toggle("collapsed")
})
})
hider.appendChild(hiderButton)
legendDescContainer.appendChild(legendDescHeader)
legendDescContainer.appendChild(legendRuDescContainer)
legendDescContainer.appendChild(legendRuHiddenDescContainer)
legendDescContainer.appendChild(legendEnDescContainer)
legendDescContainer.appendChild(legendEnHiddenDescContainer)
legendDescContainer.appendChild(hider)
legendContainer.appendChild(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 = document.createElement("div")
legendFilesContainer.classList.add("legend-container__files")
let imagesForLegendContainer = createFrom(rows, "files-container legend-photo-container", [
{ index: 23, name: "Фото в легенде" }
])
legendFilesContainer.appendChild(imagesForLegendContainer)
let imagesForHistoryContainer = createFrom(rows, "files-container history-photo-container", [
{ name: "Фото для ИС" }
])
legendFilesContainer.appendChild(imagesForHistoryContainer)
let audioForLegendContainer = createFrom(rows, "files-container legend-files-container", [
{ index: 27, name: "Файлы в легенде" }
])
legendFilesContainer.appendChild(audioForLegendContainer)
let audioForHistoryContainer = createFrom(rows, "files-container history-files-container", [
{ name: "Файлы для ИС" }
])
legendFilesContainer.appendChild(audioForHistoryContainer)
return legendFilesContainer
}
function makeAdminFiles(rows) {
let adminFilesContainer = document.createElement('div')
adminFilesContainer.classList.add('admin-files-container')
let imagesForAdminContainer = createFrom(rows, "files-container admin-photo-container", [
{ index: 31, name: "Фото в админке" }
])
adminFilesContainer.appendChild(imagesForAdminContainer)
let audioForAdminContainer = createFrom(rows, "files-container admin-files-container", [
{ index: 35, name: "Файлы в админке" }
])
adminFilesContainer.appendChild(audioForAdminContainer)
return adminFilesContainer
}
function makeBottomOptions(rows) {
return createFromMulti(rows, "options bottom-options", {
from: 59,
to: rows.length - 3
})
}
function makeDialog() {
let dialog = document.createElement("dialog")
dialog.id = "dialog"
document.body.appendChild(dialog)
dialog.addEventListener("click", e => {
if (e.target == dialog) {
e.target.close()
}
})
dialog.addEventListener("close", e => {
e.target.innerHTML = ""
})
}
function formatMap() {
if (map !== undefined && L !== undefined) {
let content = document.querySelector("#content")
let contentWrapper = document.createElement("div")
contentWrapper.id = "content-wrapper"
contentWrapper.appendChild(document.querySelector("form"))
contentWrapper.appendChild(document.querySelector("#map-wrapper"))
content.appendChild(contentWrapper)
let panToCenter = document.createElement("button")
panToCenter.type = "button"
panToCenter.textContent = "В центр"
panToCenter.addEventListener("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").appendChild(panToCenter)
}
}
function checkIfAlwaysPrettify(form, oldTable, container, insertedFileRows) {
if (localStorage.getItem(localStorageItems.ALWAYS_PRETTIFY)) {
document.querySelector(`input[name^="always-prettify-0"]`).click()
prettify(form, oldTable, container, insertedFileRows)
}
}
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 container = makeContainer()
let oldTable = document.querySelector("#props")
/* HEADER */
let headerContainer = makeHeader(rows)
/* TOP BUTTONS */
let [topButtonsContainer, bottomButtonsContainer] = makeTopAndBottomButtons(rows, form, oldTable, container, insertedFileRows)
addUglyDeleteListener()
createSendButtons()
/* LEGEND */
let legendContainer = makeLegend(rows)
legendContainer.appendChild(makeLegendFiles(rows))
/* APPEND ALL */
container.appendChild(topButtonsContainer)
container.appendChild(headerContainer)
container.appendChild(makeTopOptions(rows))
container.appendChild(makeComment(rows))
container.appendChild(legendContainer)
container.appendChild(makeAdminFiles(rows))
container.appendChild(makeBottomOptions(rows))
container.appendChild(bottomButtonsContainer)
/* MAP */
formatMap()
/* DIALOG */
makeDialog()
/* PRETTIFY CHECKBOX */
checkIfAlwaysPrettify(form, oldTable, container, insertedFileRows)
}
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() {
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)
document.querySelectorAll(`#content > form > table > tbody > tr:is(:nth-child(3), :nth-child(6)) `).forEach(el => el.remove())
document.querySelector(`#content table table tr:nth-child(2) `).remove()
}
function toggleStagePageRows(rows) {
let showIndex = [0, 3]
let to = rows.findIndex(el => el.querySelector("th")?.textContent.trim() == "Бонусы")
toggleRows(rows, 3, to, 8, showIndex)
toggleRows(rows, to, rows.length, 1, [])
}
function prettifyRouteStagesPage() {
let styles = `
tr.collapsed {
display: block;
width: 0;
height: 0;
overflow: hidden;
}
`
let rows = [...document.querySelectorAll(`#content tbody tr`)]
let collapseButton = document.createElement("button")
collapseButton.type = "button"
collapseButton.textContent = "Показать"
toggleStagePageRows(rows)
collapseButton.addEventListener("click", () => {
toggleStagePageRows(rows)
collapseButton.textContent = toggleText(collapseButton.textContent, "Показать", "Скрыть")
})
document.querySelector(`table tr:first-child th`).append(collapseButton)
addStylesToHead(styles)
}
function addCommonStyles() {
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: " | "
}
}
}
}
#content h1 {
margin-bottom: 0;
}
`)
}
/* REDIRECTS */
redirectAfterNewCpIfNeeded()
/* HEAD */
addCommonStyles()
/* SWITCH FOR DIFFERENT PAGES */
if (isCpDeletePage()) {
bindDeleteButton()
return
}
addClearBoth()
if (hasMap()) {
initMapbox()
addFullscreenButton()
if (isRouteBuildPage() || isRouteMapPage()) {
centerMap()
}
}
addStickyMenu()
if (isCpEditPage()) {
prettifyEditCpPage()
}
if (isRouteListPage()) {
addStageLink()
}
if (isRouteBuildPage()) {
prettifyRouteBuildPage()
}
if (isRouteStagesPage()) {
prettifyRouteStagesPage()
}
})();