tampermonkey/main.js
2025-03-12 13:59:43 +00:00

1527 lines
47 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
// @desc try to take over the world!
// @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';
let fileInputs = {
"attachment1": "legend_photo",
"attachment4": "legend_files",
"attachment2": "admin_photo",
"attachment3": "admin_files"
}
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("Получить координаты КП из картинки", "")
.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)
}
const sleep = ms => new Promise(res => setTimeout(res, ms));
function isEditCpPage() {
let params = new URLSearchParams(document.location.search)
return params.get("action") === "edit"
}
function isDeleteCpPage() {
let params = new URLSearchParams(document.location.search)
return params.get("action") === "delete"
}
async function updatePoint(formData) {
return await fetch(`https://runcity.geo.rictum.ru/api/competitions/${window.location.pathname.split('/')[1]}/update`, {
method: 'POST',
body: formData
})
}
async function deletePoint() {
return await fetch(`https://runcity.geo.rictum.ru/api/points/${new URLSearchParams(location.search).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) {
let formData = new FormData()
let normalInputName = 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 addDisplayedFile(inputName, type, file, preview, removeLink, purpose = null) {
if (!inputName || !file || !removeLink) return
let fileListContainer
if (purpose !== null) {
if (inputName == "attachment1" || inputName == "attachment4") {
if (purpose == "для легенды") {
let fileContainerClassName = fileInputs[inputName].replace("_", "-")
fileListContainer = document.querySelector(`.${fileContainerClassName}-container .file-list-container`)
}
else if (purpose == "для ист. справки") {
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)
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 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 fileListContainer = this.parentElement.parentElement.querySelector(".file-list-container")
let fileContainer = document.createElement("div")
fileContainer.classList.add("file-container")
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 = [...fileListContainer.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
fileListContainer.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 purpose = row.querySelector("td:nth-child(2)")?.innerHTML.match(/для[^\|]+/g)?.[0].trim()
addDisplayedFile(fileInput, type, fileUrl, preview, deleteUrl, purpose)
}
}
}
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)
$("#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)
i++
return alwaysPrettify
}
function hide(elList) {
elList.forEach(el => el.classList.add("hidden"))
}
function show(elList) {
elList.forEach(el => el.classList.remove("hidden"))
}
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 fileContainers = [...document.querySelectorAll(".file-list-container")]
let fileInputNames = ["legend_photo", "legend_files", "admin_photo", "admin_files"]
for (let [i, fileContainer] of fileContainers.entries()) {
formData.set(`cp[${fileInputNames[i]}]`, fileContainer.children.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 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"]`)
link.addEventListener("click", async e => {
e.preventDefault()
if (!confirm("Точно?")) return
await sendFileDeleted(fileInput.name.split("[")[0])
location.href = link.href
})
}
}
function addStickyHeaderToMainList() {
let topMenu = document.querySelector(".large-menu")
let table = document.querySelector("#props")
if (topMenu) {
let caption = document.createElement("caption")
caption.appendChild(topMenu)
table.prepend(caption)
}
}
function addFullscreenButton() {
$(async () => {
if (document.querySelector("#map") !== null) {
while (L.Control.Fullscreen === undefined) {
await sleep(500)
}
map.addControl(new L.Control.Fullscreen())
}
})
}
let styles = `
#props caption {
position: sticky;
top: 0;
.large-menu {
background: white;
}
}
#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="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;
}
`
/* REDIRECTS */
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]", new URLSearchParams(document.location.search).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)
}
/* HEAD */
let styleSheet = document.createElement("style")
styleSheet.textContent = styles
document.head.appendChild(styleSheet)
/* SWITCH FOR DIFFERENT PAGES */
if (isDeleteCpPage()) {
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)
return
}
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')
addStickyHeaderToMainList()
addFullscreenButton()
if (!isEditCpPage()) {
return
}
/* action=edit HEAD */
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")
/* NEW DEFALUT VALUES */
document.querySelector(`#props input[name="new_file_type1"][value="l"]`).click()
document.querySelector(`#props input[name="new_file_type4"][value="l"]`).click()
/* CONTAINER */
let form = document.querySelector('form')
let rows = [...document.querySelectorAll('#props > tbody > tr')]
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++
}
}
let container = document.createElement('div')
container.id = "new"
let oldTable = document.querySelector("#props")
/* HEADER */
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)
/* TOP BUTTONS */
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))
})
addUglyDeleteListener()
createSendButtons()
/* OPTIONS */
let firstContainer = 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: "Знак" }
])
let commentContainer = createFrom(rows, "comment", [
{ index: 9, desc: "" }
])
/* LEGEND */
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 => {
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)
/* LEGEND FILES */
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)
legendContainer.appendChild(legendFilesContainer)
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)
/* BOTTOM OPTIONS */
let bottomOptionsContainer = createFromMulti(rows, "options bottom-options", {
from: 59,
to: rows.length - 3
})
/* APPEND ALL */
container.appendChild(topButtonsContainer)
container.appendChild(headerContainer)
container.appendChild(firstContainer)
container.appendChild(commentContainer)
container.appendChild(legendContainer)
container.appendChild(adminFilesContainer)
container.appendChild(bottomOptionsContainer)
container.appendChild(bottomButtonsContainer)
/* MAP */
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)
}
/* DIALOG */
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 = ""
})
/* PRETTIFY CHECKBOX */
if (localStorage.getItem(localStorageItems.ALWAYS_PRETTIFY)) {
document.querySelector(`input[name^="always-prettify-0"]`).click()
prettify(form, oldTable, container, insertedFileRows)
}
})();