first commit

This commit is contained in:
Zhora Shalyapin 2024-11-29 12:36:39 +00:00
commit fb9168f700

867
main.js Normal file
View File

@ -0,0 +1,867 @@
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 2024-11-18
// @desc try to take over the world!
// @author You
// @match https://rst.runcity.org/msk2025/cp_mgmt/?action=edit&cp_id=88548
// @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://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js
// ==/UserScript==
(function () {
'use strict';
class Property {
id;
name;
content;
desc;
constructor(id, row) {
this.id = id
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("для легенды (\"l\")", "")
.replaceAll("общие (\"c\")", "")
.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 ? "<span title='" + desc + "'>?</span>" : ""
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 prop = new Property(options.index, rows[options.index])
container.appendChild(prop.toDiv(options.name, options.desc))
}
return container
}
function addCss(link) {
let css = document.createElement("link")
css.href = link
css.rel = "stylesheet"
document.head.appendChild(css)
}
function makeDownloadLink(name, href = null) {
let downloadLink = document.createElement("a")
downloadLink.setAttribute("download", name)
downloadLink.textContent = "Скачать"
if (href)
downloadLink.href = href
return downloadLink
}
function addDisplayedFile(input, type, file, preview, removeLink) {
if (!input || !file || !removeLink) return
let 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.addEventListener("click", () => {
let dialog = document.querySelector("dialog")
let dialogFile = displayedFile.cloneNode(true)
dialogFile.src = file
dialog.appendChild(dialogFile)
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("test.png", file))
let deleteButton = document.createElement("button")
deleteButton.type = "button"
deleteButton.classList.add("button-delete")
deleteButton.textContent = "x"
deleteButton.addEventListener("click", e => {
if (!confirm("Точно?")) return
fetch(removeLink)
.then(res => 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 handleFiles() {
let dt = new DataTransfer()
let storageLabel = `files-${this.dataset.index}`
let storedFiles = localStorage.getItem(storageLabel)
if (storedFiles) {
let oldFiles = JSON.parse(localStorage.getItem(storageLabel))
for (const oldFile of oldFiles) {
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")
dialog.appendChild(displayedFile.cloneNode(true))
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
let storedFiles = JSON.parse(localStorage.getItem(storageLabel)) ?? []
storedFiles.splice(storedFiles.length - this.files.length + index, 1)
localStorage.setItem(storageLabel, JSON.stringify(storedFiles))
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)
let storedFiles = JSON.parse(localStorage.getItem(storageLabel)) ?? []
storedFiles.push({data: displayedFile.src, name: file.name})
localStorage.setItem(storageLabel, JSON.stringify(storedFiles))
}
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", handleFiles, 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
}
addDisplayedFile(document.querySelector(`input[name="${fileInput}\[\]"]`), type, fileUrl, preview, deleteUrl)
}
}
}
function hide(elList) {
elList.forEach(el => el.classList.add("hidden"))
}
function show(elList) {
elList.forEach(el => el.classList.remove("hidden"))
}
let styles = `
#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;
}
#new > :not(:first-child) {
padding-top: 5px;
border-top: 1px solid #ddd;
}
#new > :not(:last-child) {
padding-bottom: 5px;
}
#cps_main {
width: 150px;
}
input[type=submit] {
width: fit-content;
}
button, input[type=submit] {
cursor: pointer;
}
#new input[type=radio], input[type=checkbox] {
margin: 0;
}
#new input[type="file"] {
display: none;
}
#new div > input[type="checkbox"] {
display: flex;
align-items: center;
}
.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);
}
.header > * {
display: flex;
gap: 5px;
}
:is(.header, .options) input {
width: unset;
}
.header {
display: flex;
gap: 30px;
}
.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] {
display: none;
}
.file-list-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 5px;
}
.file-container {
display: flex;
flex-direction: column;
gap: 5px;
}
img.preview {
width: 120px;
}
img.preview-small {
width: 60px;
}
.files-container img.preview {
cursor: zoom-in;
}
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;
}
`
/* HEAD */
let styleSheet = document.createElement("style")
styleSheet.textContent = styles
document.head.appendChild(styleSheet)
addCss("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css")
addCss('https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.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: "№" },
{ index: 8, name: "Название" }
])
let copyLink = document.createElement("div")
copyLink.innerHTML = new Property(2, 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 => {
let prettifyButton = document.createElement("button")
prettifyButton.type = "button"
prettifyButton.textContent = "Сделать красиво"
prettifyButton.addEventListener("click", () => {
let inputValues = saveInputValues(oldTable)
form.removeChild(oldTable)
form.appendChild(container)
setInputValues(container, inputValues)
prettifyFiles(insertedFileRows)
$("#cps_main").select2()
})
el.appendChild(prettifyButton)
})
;[topButtonsContainer, bottomButtonsContainer].forEach(el => {
let unglifyButton = document.createElement("button")
unglifyButton.type = "button"
unglifyButton.textContent = "Сделать некрасиво"
unglifyButton.addEventListener("click", () => {
let inputValues = saveInputValues(container)
form.removeChild(container)
form.appendChild(oldTable)
setInputValues(oldTable, inputValues)
})
el.querySelector("div > div > div").appendChild(unglifyButton)
})
/* OPTIONS */
let firstContainer = createFrom(rows, "options", [
{ index: 3 },
{ index: 4, name: "КП-загадка" },
{ index: 5, name: "Старт" },
{ index: 6, name: "Финиш" },
{ index: 16, name: "Этапник" },
{ index: 7, name: "Пиктограмма" },
{ index: 12, name: "Нужна ИС" },
{ index: 17, name: "Спрятать ИС" },
{ index: 14, name: "Основной КП" },
{ index: 10, name: "X" },
{ index: 11, name: "Y" },
{ 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")
let legendEnSwitchContainer = createFrom(rows, "legend-switch-container", [
{ index: 48 }
])
legendEnSwitchContainer.addEventListener("click", event => {
hide([legendRuDescContainer, legendRuHiddenDescContainer, legendEnSwitchContainer])
show([legendEnDescContainer, legendEnHiddenDescContainer, legendRuSwitchContainer])
})
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])
})
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: "" }
])
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: "" }
])
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", [
{ index: 23, name: "Фото в легенде" }
])
legendFilesContainer.appendChild(imagesForLegendContainer)
let audioForLegendContainer = createFrom(rows, "files-container", [
{ index: 27, name: "Файлы в легенде" }
])
legendFilesContainer.appendChild(audioForLegendContainer)
legendContainer.appendChild(legendFilesContainer)
let adminFilesContainer = document.createElement('div')
adminFilesContainer.classList.add('admin-files-container')
let imagesForAdminContainer = createFrom(rows, "files-container", [
{ index: 31, name: "Фото в админке" }
])
adminFilesContainer.appendChild(imagesForAdminContainer)
let audioForAdminContainer = createFrom(rows, "files-container", [
{ index: 35, name: "Файлы в админке" }
])
adminFilesContainer.appendChild(audioForAdminContainer)
/* BOTTOM OPTIONS */
let bottomOptionsContainer = createFrom(rows, "options bottom-options", [
{ index: 59 },
{ index: 60 },
{ index: 61 },
{ index: 62 },
{ index: 63 }
])
/* 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 */
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)
$(() => {
map.addControl(new L.Control.Fullscreen())
})
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)), 13)
})
document.querySelector("#map_controls").appendChild(panToCenter)
/* FILES */
localStorage.clear()
let dialog = document.createElement("dialog")
dialog.id = "dialog"
document.body.appendChild(dialog)
dialog.addEventListener("click", (e) => {
if (e.target == dialog) {
e.target.close()
e.target.innerHTML = ""
}
})
})();