// ==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("
", "") .replaceAll(/\s*Перейти к легенде.*$/gs, "") .replaceAll(`общие ("c")`, "") .replaceAll(`("l")`, "") .replaceAll(`("h")`, "") .replaceAll("Получить координаты КП из картинки", "") .trim() } if (columns.length === 3) { this.desc = columns[2].innerHTML.replaceAll("
", "").replaceAll(/(\n)\s*/g, "$1").trim() } } toDiv(altName = null, altDesc = null) { let div = document.createElement('div') let desc = altDesc ?? this.desc desc = desc ? "
" + "" + "
" : "" div.innerHTML = "" + "
" + (this.content ?? "") + "
" + 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 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 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 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 = `/${getCompetition()}/${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 => { 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 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") /* 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() } let styles = ` .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; } #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; } ` /* REDIRECTS */ redirectAfterNewCpIfNeeded() /* HEAD */ addStylesToHead(styles) /* 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() } })();