Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b95f467336 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
test.js
|
||||
80
py/build.py
Normal file
80
py/build.py
Normal file
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Сборщик итогового файла test.js.
|
||||
|
||||
Берёт файл tpl.js и заменяет в нём строку-заглушку "code;" на
|
||||
объединённое содержимое всех файлов из папки src (в алфавитном порядке).
|
||||
Результат сохраняется в файл test.js.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Файлы, участвующие в сборке (пути относительно скрипта)
|
||||
TPL_FILE = "tpl.js"
|
||||
SRC_DIR = "src"
|
||||
OUTPUT_FILE = "test.js"
|
||||
|
||||
# Строка-заглушка, которую заменяем на код из src
|
||||
PLACEHOLDER = "code;"
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def read_file(path):
|
||||
"""Читает файл и возвращает его содержимое в виде строки."""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write_file(path, content):
|
||||
"""Записывает содержимое в файл."""
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def build():
|
||||
"""Собирает итоговый файл test.js из tpl.js и содержимого src/."""
|
||||
# Читаем шаблон
|
||||
tpl_path = os.path.join(BASE_DIR, TPL_FILE)
|
||||
if not os.path.isfile(tpl_path):
|
||||
raise FileNotFoundError("Не найден файл шаблона: {}".format(TPL_FILE))
|
||||
template = read_file(tpl_path)
|
||||
|
||||
# Читаем файлы из папки src в алфавитном порядке
|
||||
src_path = os.path.join(BASE_DIR, SRC_DIR)
|
||||
parts = []
|
||||
if os.path.isdir(src_path):
|
||||
src_files = sorted(
|
||||
f for f in os.listdir(src_path)
|
||||
if os.path.isfile(os.path.join(src_path, f))
|
||||
)
|
||||
for name in src_files:
|
||||
parts.append(read_file(os.path.join(src_path, name)))
|
||||
else:
|
||||
print("Предупреждение: не найдена папка: {}".format(SRC_DIR + "/"))
|
||||
|
||||
code_content = "\n".join(parts)
|
||||
|
||||
# Проверяем наличие заглушки
|
||||
if PLACEHOLDER not in template:
|
||||
raise ValueError(
|
||||
"В шаблоне {} не найдена строка-заглушка '{}'".format(
|
||||
TPL_FILE, PLACEHOLDER
|
||||
)
|
||||
)
|
||||
|
||||
# Заменяем заглушку на объединённый код src
|
||||
result = template.replace(PLACEHOLDER, code_content)
|
||||
|
||||
# Записываем результат
|
||||
output_path = os.path.join(BASE_DIR, OUTPUT_FILE)
|
||||
write_file(output_path, result)
|
||||
|
||||
print("Готово: {} -> {} ({} символов)".format(
|
||||
", ".join([TPL_FILE, SRC_DIR + "/*"]),
|
||||
OUTPUT_FILE,
|
||||
len(result),
|
||||
))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
53
py/split.py
Normal file
53
py/split.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Разбивает src/all.js на несколько файлов по заданным диапазонам строк.
|
||||
|
||||
Диапазоны указаны как (start, end) включительно, в порядке добавления.
|
||||
Файлы создаются в src/, затем удаляется исходный src/all.js.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC = os.path.join(BASE_DIR, "src")
|
||||
SOURCE = os.path.join(SRC, "all.js")
|
||||
|
||||
# (filename, [(start, end), ...])
|
||||
PLAN = [
|
||||
("00-config.js", [(1, 72)]),
|
||||
("10-dom.js", [(74, 122)]),
|
||||
("20-property.js", [(124, 239)]),
|
||||
("30-utils.js", [(241, 302), (601, 626), (899, 922)]),
|
||||
("40-api.js", [(304, 363)]),
|
||||
("50-common-ui.js", [(365, 382), (462, 469), (924, 999), (1149, 1259), (1261, 1265), (1627, 1642)]),
|
||||
("60-cp-files.js", [(471, 599), (628, 858), (1131, 1147)]),
|
||||
("70-cp-edit.js", [(384, 460), (862, 897), (1001, 1129), (1320, 1443), (1445, 1625), (1644, 2103)]),
|
||||
("75-map.js", [(1267, 1285), (2105, 2128)]),
|
||||
("80-route-build.js", [(2130, 2386)]),
|
||||
("90-route-edit.js", [(2388, 2555)]),
|
||||
("100-route-stages.js", [(1287, 1318), (2557, 2614)]),
|
||||
("110-main.js", [(2616, 2821)]),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
with open(SOURCE, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
total = len(lines)
|
||||
for name, ranges in PLAN:
|
||||
parts = []
|
||||
for start, end in ranges:
|
||||
if start < 1 or end > total or start > end:
|
||||
raise ValueError(
|
||||
"{}: неверный диапазон {}-{} (всего строк {})".format(name, start, end, total)
|
||||
)
|
||||
parts.append("".join(lines[start - 1:end]))
|
||||
content = "\n".join(parts).rstrip("\n") + "\n"
|
||||
path = os.path.join(SRC, name)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("OK {} (строк в файле: {})".format(name, content.count("\n")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
72
src/00-config.js
Normal file
72
src/00-config.js
Normal file
@ -0,0 +1,72 @@
|
||||
const adminSite = "runcity.geo.rictum.ru"
|
||||
|
||||
const adminApiKey = "2|KaYFQk0txpFivlhphdTHDlS37uat8ILuCbVR6HxH9dd21a66"
|
||||
|
||||
const links = {
|
||||
"Волонтеры": "suv_comp",
|
||||
"Контрольные пункты": "cp_mgmt",
|
||||
"+ КП": "cp_mgmt/?action=edit",
|
||||
"Маршруты": "route_mgmt",
|
||||
"Тесты": "route_mgmt?test=1",
|
||||
"+ Тест": "suv_comp?action=test_online",
|
||||
"Склеить": "suv_comp?action=test_merge",
|
||||
"Прохождение": "callcenter?action=online_log",
|
||||
"КП в трассах": "route_mgmt?action=cp_list",
|
||||
"Связанные КП": relatedPointsHref(),
|
||||
}
|
||||
|
||||
const catLinks = {
|
||||
"Редактор маршрута": (catId) => `route_mgmt?action=edit&cat_id=${catId}`,
|
||||
"Конструктор маршрута": (catId) => `route_mgmt/?action=build2&cat_id=${catId}`,
|
||||
"Карта": (catId) => `route_mgmt?action=map&cat_id=${catId}`,
|
||||
"Этапы": (catId) => `route_mgmt/?action=stages&cat_id=${catId}`,
|
||||
"Легенда": (catId) => `route_mgmt?action=preview&cat_id=${catId}&locale_id=ru`,
|
||||
}
|
||||
|
||||
const fileInputs = {
|
||||
"attachment1": {
|
||||
"l": "legend_photo",
|
||||
"h": "history_photo"
|
||||
},
|
||||
"attachment4": {
|
||||
"l": "legend_files",
|
||||
"h": "history_files"
|
||||
},
|
||||
"attachment2": "admin_photo",
|
||||
"attachment3": "admin_files"
|
||||
}
|
||||
|
||||
const fileInpitVariants = {
|
||||
"l": "для легенды",
|
||||
"h": "для ист. справки"
|
||||
}
|
||||
|
||||
const mapsCenterByCompetition = {
|
||||
"msk2025": {
|
||||
lat: 55.839808,
|
||||
ifSouthern: {
|
||||
lat: 55.798531,
|
||||
lon: 37.690380,
|
||||
},
|
||||
ifEverywhere: {
|
||||
lat: 55.839759,
|
||||
lon: 37.706577
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const localStorageItems = {
|
||||
NEED_UPDATE_ID: "needUpdateId",
|
||||
JUST_CREATED: "justCreated",
|
||||
PREV_CREATED: "prevCreated",
|
||||
LATTITUDE: "lattitude",
|
||||
LONGITUDE: "longitude",
|
||||
NOT_PRETTIFY_EDIT_CP: "notPrettifyEditCp",
|
||||
REDIRECT_EXIT: "redirectExit",
|
||||
DISABLED_PAGES: "disabledPages"
|
||||
}
|
||||
|
||||
const ZOOM = 17
|
||||
const METERS = 510
|
||||
|
||||
const stageColors = ["#ff8080", "#ffc680", "#ffff80", "#80ff80", "#80ffff", "#8080ff", "#ff80ff"]
|
||||
49
src/10-dom.js
Normal file
49
src/10-dom.js
Normal file
@ -0,0 +1,49 @@
|
||||
class Tag {
|
||||
static make(name, params = {}) {
|
||||
let element = document.createElement(name)
|
||||
for (const [name, value] of Object.entries(params)) {
|
||||
if (name == "on") {
|
||||
for (const [event, listener] of Object.entries(value)) {
|
||||
element.addEventListener(event, listener)
|
||||
}
|
||||
}
|
||||
else if (name == "classes") {
|
||||
element.classList.add(...value.split(" "))
|
||||
}
|
||||
else if (name == "children") {
|
||||
element.append(...value)
|
||||
}
|
||||
else if (name.startsWith("_")) {
|
||||
element.setAttribute(name.substring(1), value)
|
||||
}
|
||||
else
|
||||
element[name] = value
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
static div(params) {
|
||||
return this.make("div", params)
|
||||
}
|
||||
|
||||
static span(params) {
|
||||
return this.make("span", params)
|
||||
}
|
||||
|
||||
static button(params) {
|
||||
return this.make("button", params)
|
||||
}
|
||||
|
||||
static input(params) {
|
||||
return this.make("input", params)
|
||||
}
|
||||
|
||||
static a(params) {
|
||||
return this.make("a", params)
|
||||
}
|
||||
|
||||
static img(params) {
|
||||
return this.make("img", params)
|
||||
}
|
||||
}
|
||||
91
src/100-route-stages.js
Normal file
91
src/100-route-stages.js
Normal file
@ -0,0 +1,91 @@
|
||||
function addStageLink() {
|
||||
let linkCells = [...document.querySelectorAll("#content table td:nth-child(2)")]
|
||||
for (const linkCell of linkCells) {
|
||||
let href = linkCell.querySelector("a").href
|
||||
let catId = new URLSearchParams(href).get("cat_id")
|
||||
let constructorLink = linkCell.querySelector(`a[href="${catLinks["Конструктор маршрута"](catId).replace("route_mgmt/", "")}"]`)
|
||||
|
||||
if (constructorLink) {
|
||||
let innerText = "Этапы"
|
||||
let stagesLink = Tag.a({
|
||||
innerText: innerText,
|
||||
href: catLinks[innerText](catId)
|
||||
})
|
||||
|
||||
constructorLink.after(stagesLink)
|
||||
constructorLink.after(". ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideNonTesters() {
|
||||
if (urlParams().get("test") == null) return
|
||||
|
||||
document.querySelectorAll(`td:nth-child(2)`).forEach(el => {
|
||||
let innerHTML = el.innerHTML
|
||||
let from = innerHTML.indexOf("Отчёты тестировщиков")
|
||||
if (from == -1)
|
||||
from = innerHTML.length
|
||||
|
||||
el.innerHTML = innerHTML.substring(from)
|
||||
})
|
||||
}
|
||||
|
||||
function toggleStagePageRows(rows) {
|
||||
let showIndex = [0, 3]
|
||||
let to = rows.findIndex(el => el.querySelector("th")?.textContent.trim() == "Бонусы")
|
||||
if (to == -1)
|
||||
to = rows.length
|
||||
|
||||
toggleRows(rows, 3, to, 8, showIndex)
|
||||
toggleRows(rows, to, rows.length, 1, [])
|
||||
}
|
||||
|
||||
function hideUselessRowsFromRouteStagesPage() {
|
||||
let styles = `
|
||||
tr.collapsed {
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
|
||||
let rows = [...document.querySelectorAll(`#content tbody tr`)]
|
||||
let collapseButton = Tag.button({
|
||||
type: "button",
|
||||
textContent: "Показать",
|
||||
on: {
|
||||
click: () => {
|
||||
toggleStagePageRows(rows)
|
||||
collapseButton.textContent = toggleText(collapseButton.textContent, "Показать", "Скрыть")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
toggleStagePageRows(rows)
|
||||
|
||||
document.querySelector(`table tr:first-child th`).append(collapseButton)
|
||||
|
||||
addStylesToHead(styles)
|
||||
}
|
||||
|
||||
function addCpNameToOptions() {
|
||||
let options = [...document.querySelectorAll(`option`)]
|
||||
let cpIds = [...new Set(
|
||||
options
|
||||
.map($option => $option.value)
|
||||
.filter($value => $value != null)
|
||||
)
|
||||
]
|
||||
|
||||
;(async () => {
|
||||
let points = await getPointsByField("cp_id", cpIds)
|
||||
|
||||
for (const option of options) {
|
||||
if (option.value != null && points[option.value] != null) {
|
||||
option.innerText = option.innerText + " - " + points[option.value].name_int
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
206
src/110-main.js
Normal file
206
src/110-main.js
Normal file
@ -0,0 +1,206 @@
|
||||
function addCommonStyles() {
|
||||
addStylesToHead(`
|
||||
#content h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function getDisabledPages() {
|
||||
return JSON.parse(localStorage.getItem(localStorageItems.DISABLED_PAGES) ?? "{}")
|
||||
}
|
||||
|
||||
function saveDisabledPages(pages) {
|
||||
localStorage.setItem(localStorageItems.DISABLED_PAGES, JSON.stringify(pages))
|
||||
}
|
||||
|
||||
function isPageDisabled() {
|
||||
let disabledPages = getDisabledPages()
|
||||
let pageType = getPageType()
|
||||
|
||||
return disabledPages[pageType]?.[getAction()]
|
||||
}
|
||||
|
||||
function addDisabledPage() {
|
||||
let disabledPages = getDisabledPages()
|
||||
let pageType = getPageType()
|
||||
if (!(pageType in disabledPages)) {
|
||||
disabledPages[pageType] = {}
|
||||
}
|
||||
disabledPages[pageType][getAction()] = true
|
||||
|
||||
saveDisabledPages(disabledPages)
|
||||
}
|
||||
|
||||
function removeDisabledPage() {
|
||||
let disabledPages = getDisabledPages()
|
||||
let pageType = getPageType()
|
||||
if (!(pageType in disabledPages)) {
|
||||
return
|
||||
}
|
||||
delete disabledPages[pageType][getAction()]
|
||||
|
||||
saveDisabledPages(disabledPages)
|
||||
}
|
||||
|
||||
function addEnableButtons() {
|
||||
let enableButton = Tag.button({
|
||||
type: "button",
|
||||
id: "enable-button",
|
||||
textContent: "Сделать красиво",
|
||||
on: {
|
||||
click: () => {
|
||||
if (pretty) return
|
||||
|
||||
hide([enableButton])
|
||||
prettify()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!isPageDisabled()) {
|
||||
hide([enableButton])
|
||||
}
|
||||
|
||||
let header = document.querySelector("#header")
|
||||
header.insertBefore(enableButton, header.querySelector("#globalmenu"))
|
||||
header.insertBefore(createAlwaysEnable(), header.querySelector("#globalmenu"))
|
||||
|
||||
addStylesToHead(`
|
||||
#always-enable-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function addDisableButton() {
|
||||
let disableButton = Tag.button({
|
||||
id: "disable-button",
|
||||
type: "button",
|
||||
textContent: "Сделать как было",
|
||||
on: {
|
||||
click: () => {
|
||||
if (!pretty) return
|
||||
|
||||
if (!confirm("Это действие отключит скрипт на этой странице и удалит все несохраненные данные")) return
|
||||
|
||||
addDisabledPage()
|
||||
pretty = false
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
header.querySelector("#enable-button").after(disableButton)
|
||||
}
|
||||
|
||||
function createAlwaysEnable() {
|
||||
let alwaysEnableContainer = Tag.div({
|
||||
id: "always-enable-container",
|
||||
children: [
|
||||
Tag.make("label", {
|
||||
_for: "always-enable",
|
||||
textContent: "Всегда красиво"
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
let alwaysEnableCheckbox = Tag.input({
|
||||
type: "checkbox",
|
||||
id: "always-enable",
|
||||
name: "always-enable",
|
||||
on: {
|
||||
change: function () {
|
||||
if (this.checked)
|
||||
removeDisabledPage()
|
||||
else
|
||||
addDisabledPage()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!isPageDisabled()) {
|
||||
alwaysEnableCheckbox.checked = true
|
||||
}
|
||||
|
||||
alwaysEnableContainer.append(alwaysEnableCheckbox)
|
||||
|
||||
return alwaysEnableContainer
|
||||
}
|
||||
|
||||
function prettify() {
|
||||
if (pretty) return
|
||||
|
||||
pretty = true
|
||||
|
||||
addDisableButton()
|
||||
|
||||
addCommonStyles()
|
||||
|
||||
/* SWITCH FOR DIFFERENT PAGES */
|
||||
|
||||
addStickyMenu()
|
||||
|
||||
if (isCpEditPage()) {
|
||||
prettifyEditCpPage()
|
||||
}
|
||||
|
||||
if (isRouteListPage()) {
|
||||
addStageLink()
|
||||
hideNonTesters()
|
||||
}
|
||||
|
||||
if (isRouteBuildPage()) {
|
||||
prettifyRouteBuildPage()
|
||||
}
|
||||
|
||||
if (isRouteEditPage()) {
|
||||
prettifyRouteEditPage()
|
||||
}
|
||||
|
||||
if (isRouteStagesPage()) {
|
||||
hideUselessRowsFromRouteStagesPage()
|
||||
addCpNameToOptions()
|
||||
}
|
||||
}
|
||||
|
||||
function prettifyIfEnabled() {
|
||||
if (isPageDisabled()) return
|
||||
|
||||
prettify()
|
||||
}
|
||||
|
||||
/* REDIRECTS */
|
||||
|
||||
redirectAfterNewCpIfNeeded()
|
||||
|
||||
/* PRETTIFY */
|
||||
|
||||
let pretty = false
|
||||
|
||||
addEnableButtons()
|
||||
|
||||
if (isCpDeletePage()) {
|
||||
bindDeleteButton()
|
||||
return
|
||||
}
|
||||
|
||||
prettifyIfEnabled()
|
||||
|
||||
/* MAPS */
|
||||
|
||||
addClearBoth()
|
||||
|
||||
if (hasMap()) {
|
||||
initMapbox()
|
||||
addFullscreenButton()
|
||||
|
||||
if (isRouteBuildPage() || isRouteMapPage()) {
|
||||
centerMap()
|
||||
}
|
||||
}
|
||||
116
src/20-property.js
Normal file
116
src/20-property.js
Normal file
@ -0,0 +1,116 @@
|
||||
class Property {
|
||||
name
|
||||
content
|
||||
desc
|
||||
|
||||
constructor(row = null) {
|
||||
if (row == null) return
|
||||
|
||||
let columns = [...row.querySelectorAll('td, th')].map(el => el.cloneNode(true))
|
||||
if (columns.length === 1) {
|
||||
this.content = columns[0].innerHTML.trim()
|
||||
}
|
||||
else if (columns.length >= 2) {
|
||||
this.name = columns[0].innerHTML.trim()
|
||||
let elementsToRemove = columns[1].querySelector("input[name=\"read_gps2\"]")
|
||||
if (elementsToRemove) {
|
||||
columns[1].removeChild(elementsToRemove)
|
||||
}
|
||||
|
||||
this.content = columns[1].innerHTML
|
||||
.replaceAll("<br>", "")
|
||||
.replaceAll(/\s*Перейти к легенде.*$/gs, "")
|
||||
.replaceAll(`общие ("c")`, "")
|
||||
.replaceAll(`("l")`, "")
|
||||
.replaceAll(`("h")`, "")
|
||||
.replaceAll("Получить координаты КП из картинки", "")
|
||||
.trim()
|
||||
}
|
||||
|
||||
if (columns.length === 3) {
|
||||
this.desc = columns[2].innerHTML.replaceAll("<br>", "").replaceAll(/(\n)\s*/g, "$1").trim()
|
||||
}
|
||||
}
|
||||
|
||||
toDiv(altName = null, altDesc = null) {
|
||||
let desc = altDesc ?? this.desc
|
||||
let descTooltip = desc ? Tag.div({
|
||||
classes: 'desc-icon-container',
|
||||
title: desc,
|
||||
children: [
|
||||
Tag.img({
|
||||
classes: 'desc-icon',
|
||||
src: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnIGlkPSJzdmcyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAwIiB3aWR0aD0iMjAwIiB2ZXJzaW9uPSIxLjAiPgogPHBhdGggaWQ9InBhdGgyMzgyIiBkPSJtMTY1LjMzIDExMy40NGExMDMuNjEgMTAzLjYxIDAgMSAxIC0yMDcuMjIgMCAxMDMuNjEgMTAzLjYxIDAgMSAxIDIwNy4yMiAweiIgdHJhbnNmb3JtPSJtYXRyaXgoLjkzNzM5IDAgMCAuOTM3MzkgNDIuMTQzIC02LjMzOTIpIiBzdHJva2Utd2lkdGg9IjAiIGZpbGw9IiNmZmYiLz4KIDxnIGlkPSJsYXllcjEiPgogIDxwYXRoIGlkPSJwYXRoMjQxMyIgZD0ibTEwMCAwYy01NS4yIDAtMTAwIDQ0LjgtMTAwIDEwMC01LjA0OTVlLTE1IDU1LjIgNDQuOCAxMDAgMTAwIDEwMHMxMDAtNDQuOCAxMDAtMTAwLTQ0LjgtMTAwLTEwMC0xMDB6bTAgMTIuODEyYzQ4LjEzIDAgODcuMTkgMzkuMDU4IDg3LjE5IDg3LjE4OHMtMzkuMDYgODcuMTktODcuMTkgODcuMTktODcuMTg4LTM5LjA2LTg3LjE4OC04Ny4xOSAzOS4wNTgtODcuMTg4IDg3LjE4OC04Ny4xODh6bTEuNDcgMjEuMjVjLTUuNDUgMC4wMy0xMC42NTMgMC43MzctMTUuMjgyIDIuMDYzLTQuNjk5IDEuMzQ2LTkuMTI2IDMuNDg0LTEyLjg3NiA2LjIxOS0zLjIzOCAyLjM2Mi02LjMzMyA1LjM5MS04LjY4NyA4LjUzMS00LjE1OSA1LjU0OS02LjQ2MSAxMS42NTEtNy4wNjMgMTguNjg3LTAuMDQgMC40NjgtMC4wNyAwLjg2OC0wLjA2MiAwLjg3NiAwLjAxNiAwLjAxNiAyMS43MDIgMi42ODcgMjEuODEyIDIuNjg3IDAuMDUzIDAgMC4xMTMtMC4yMzQgMC4yODItMC45MzcgMS45NDEtOC4wODUgNS40ODYtMTMuNTIxIDEwLjk2OC0xNi44MTMgNC4zMi0yLjU5NCA5LjgwOC0zLjYxMiAxNS43NzgtMi45NjkgMi43NCAwLjI5NSA1LjIxIDAuOTYgNy4zOCAyIDIuNzEgMS4zMDEgNS4xOCAzLjM2MSA2Ljk0IDUuODEzIDEuNTQgMi4xNTYgMi40NiA0LjU4NCAyLjc1IDcuMzEyIDAuMDggMC43NTkgMC4wNSAyLjQ4LTAuMDMgMy4yMTktMC4yMyAxLjgyNi0wLjcgMy4zNzgtMS41IDQuOTY5LTAuODEgMS41OTctMS40OCAyLjUxNC0yLjc2IDMuODEyLTIuMDMgMi4wNzctNS4xOCA0LjgyOS0xMC43OCA5LjQwNy0zLjYgMi45NDQtNi4wNCA1LjE1Ni04LjEyIDcuMzQzLTQuOTQzIDUuMTc5LTcuMTkxIDkuMDY5LTguNTY0IDE0LjcxOS0wLjkwNSAzLjcyLTEuMjU2IDcuNTUtMS4xNTYgMTMuMTkgMC4wMjUgMS40IDAuMDYyIDIuNzMgMC4wNjIgMi45N3YwLjQzaDIxLjU5OGwwLjAzLTIuNGMwLjAzLTMuMjcgMC4yMS01LjM3IDAuNTYtNy40MSAwLjU3LTMuMjcgMS40My01IDMuOTQtNy44MSAxLjYtMS44IDMuNy0zLjc2IDYuOTMtNi40NyA0Ljc3LTMuOTkxIDguMTEtNi45OSAxMS4yNi0xMC4xMjUgNC45MS00LjkwNyA3LjQ2LTguMjYgOS4yOC0xMi4xODcgMS40My0zLjA5MiAyLjIyLTYuMTY2IDIuNDYtOS41MzIgMC4wNi0wLjgxNiAwLjA3LTMuMDMgMC0zLjk2OC0wLjQ1LTcuMDQzLTMuMS0xMy4yNTMtOC4xNS0xOS4wMzItMC44LTAuOTA5LTIuNzgtMi44ODctMy43Mi0zLjcxOC00Ljk2LTQuMzk0LTEwLjY5LTcuMzUzLTE3LjU2LTkuMDk0LTQuMTktMS4wNjItOC4yMy0xLjYtMTMuMzUtMS43NS0wLjc4LTAuMDIzLTEuNTktMC4wMzYtMi4zNy0wLjAzMnptLTEwLjkwOCAxMDMuNnYyMmgyMS45OTh2LTIyaC0yMS45OTh6Ii8+CiA8L2c+Cjwvc3ZnPgo='
|
||||
})
|
||||
]
|
||||
}) : ""
|
||||
|
||||
return Tag.div({
|
||||
children: [
|
||||
Tag.make("label", {
|
||||
_for: '',
|
||||
innerHTML: altName ?? this.name ?? ""
|
||||
}),
|
||||
Tag.div({
|
||||
innerHTML: this.content ?? "",
|
||||
}),
|
||||
descTooltip
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function createFrom(rows, classList, data) {
|
||||
let container = Tag.div({
|
||||
classes: classList
|
||||
})
|
||||
for (const options of data) {
|
||||
let div
|
||||
if (options.index != null) {
|
||||
let prop = new Property(rows[options.index])
|
||||
div = prop.toDiv(options.name, options.desc)
|
||||
}
|
||||
else {
|
||||
let prop = new Property()
|
||||
div = prop.toDiv(options.name)
|
||||
}
|
||||
container.append(div)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
function createFromMulti(rows, classList, data) {
|
||||
let rowData = []
|
||||
let toIndex = data.to ?? rows.length - 1
|
||||
for (let i = data.from; i <= toIndex; i++) {
|
||||
rowData.push({ index: i })
|
||||
}
|
||||
|
||||
return createFrom(rows, classList, rowData)
|
||||
}
|
||||
|
||||
function addCss(link) {
|
||||
document.head.append(
|
||||
Tag.make("link", {
|
||||
href: link,
|
||||
rel: "stylesheet"
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function addJs(link) {
|
||||
document.head.append(
|
||||
Tag.make("script", {
|
||||
src: link
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function addStylesToHead(styles) {
|
||||
document.head.append(
|
||||
Tag.make("style", {
|
||||
textContent: styles
|
||||
})
|
||||
)
|
||||
}
|
||||
114
src/30-utils.js
Normal file
114
src/30-utils.js
Normal file
@ -0,0 +1,114 @@
|
||||
const sleep = ms => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
function pathNameSplit() {
|
||||
return window.location.pathname.split('/')
|
||||
}
|
||||
|
||||
function getCompetition() {
|
||||
return pathNameSplit()[1]
|
||||
}
|
||||
|
||||
function getPageType() {
|
||||
return pathNameSplit()[2]
|
||||
}
|
||||
|
||||
function urlParams() {
|
||||
return new URLSearchParams(location.search)
|
||||
}
|
||||
|
||||
function getAction() {
|
||||
let params = urlParams()
|
||||
return params.get("action")
|
||||
}
|
||||
|
||||
function isCpManagement() {
|
||||
return getPageType() === "cp_mgmt"
|
||||
}
|
||||
|
||||
function isRouteManagement() {
|
||||
return getPageType() === "route_mgmt"
|
||||
}
|
||||
|
||||
function isCpListPage() {
|
||||
return isCpManagement() && getAction() === null
|
||||
}
|
||||
|
||||
function isCpEditPage() {
|
||||
return isCpManagement() && getAction() === "edit"
|
||||
}
|
||||
|
||||
function isCpDeletePage() {
|
||||
return isCpManagement() && getAction() === "delete"
|
||||
}
|
||||
|
||||
function isRouteListPage() {
|
||||
return isRouteManagement() && getAction() === null
|
||||
}
|
||||
|
||||
function isRouteBuildPage() {
|
||||
return isRouteManagement() && getAction() === "build2"
|
||||
}
|
||||
|
||||
function isRouteEditPage() {
|
||||
return isRouteManagement() && getAction() === "edit"
|
||||
}
|
||||
|
||||
function isRouteStagesPage() {
|
||||
return isRouteManagement() && getAction() === "stages"
|
||||
}
|
||||
|
||||
function isRouteMapPage() {
|
||||
return isRouteManagement() && getAction() === "map"
|
||||
}
|
||||
|
||||
function getAttachmentIndex(input) {
|
||||
return input.name.replace(/\D/g, '')
|
||||
}
|
||||
|
||||
function getContainersByVariant(variant, attachmentIndex) {
|
||||
let invertedVariant = variant == "l" ? "h" : "l"
|
||||
let variants = fileInputs[`attachment${attachmentIndex}`]
|
||||
|
||||
return [document.querySelector(`[data-container="${variants[invertedVariant]}"]`), document.querySelector(`[data-container="${variants[variant]}"]`)]
|
||||
}
|
||||
|
||||
function makeFileListContainer() {
|
||||
return Tag.div({
|
||||
classes: "file-list-container"
|
||||
})
|
||||
}
|
||||
|
||||
function getFileListContainer(filesContainer) {
|
||||
let fileListContainer = filesContainer.querySelector(".file-list-container")
|
||||
if (fileListContainer == null) {
|
||||
fileListContainer = makeFileListContainer()
|
||||
filesContainer.append(fileListContainer)
|
||||
}
|
||||
|
||||
return fileListContainer
|
||||
}
|
||||
|
||||
function hide(elList) {
|
||||
elList.forEach(el => el.classList.add("hidden"))
|
||||
}
|
||||
|
||||
function show(elList) {
|
||||
elList.forEach(el => el.classList.remove("hidden"))
|
||||
}
|
||||
|
||||
function toggleRows(rows, from, to, cycle, showIndex) {
|
||||
for (let i = from; i < to; i += cycle) {
|
||||
for (let j = 0; j < cycle; j++) {
|
||||
if (!showIndex.includes(j)) {
|
||||
rows[i + j]?.classList.toggle("collapsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleText(current, ...variants) {
|
||||
let index = variants.indexOf(current)
|
||||
if (index == -1) return null
|
||||
|
||||
return variants[index + 1 < variants.length ? index + 1 : 0]
|
||||
}
|
||||
60
src/40-api.js
Normal file
60
src/40-api.js
Normal file
@ -0,0 +1,60 @@
|
||||
function cpLink(cpId) {
|
||||
return `/${getCompetition()}/cp_mgmt/?action=edit&cp_id=${cpId}`
|
||||
}
|
||||
|
||||
async function fetchApi(route, params) {
|
||||
return await fetch(`https://${adminSite}/api/${route}`, Object.assign({
|
||||
headers: new Headers({
|
||||
'Authorization': `Bearer ${adminApiKey}`,
|
||||
}),
|
||||
}, params))
|
||||
}
|
||||
|
||||
async function updatePoint(formData) {
|
||||
return await fetchApi(`competitions/${getCompetition()}/update`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
}
|
||||
|
||||
async function deletePoint() {
|
||||
return await fetchApi(`points/${urlParams().get("cp_id")}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
async function getPointsByField(field, values) {
|
||||
let params = new URLSearchParams()
|
||||
for (const value of values) {
|
||||
params.append(`${field}[]`, value)
|
||||
}
|
||||
let response = await fetchApi(`competitions/${getCompetition()}/points?${params.toString()}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function updateRoute(formData) {
|
||||
return await fetchApi(`competitions/${getCompetition()}/route`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
}
|
||||
|
||||
async function getRoutes() {
|
||||
let response = await fetchApi(`points/${urlParams().get("cp_id")}/routes`, {
|
||||
method: 'GET'
|
||||
})
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function getUnknownPoints() {
|
||||
let response = await fetchApi(`competitions/${getCompetition()}/routes/${urlParams().get("cat_id")}/unknown`, {
|
||||
method: 'GET'
|
||||
})
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
function relatedPointsHref() {
|
||||
return `https://${adminSite}/competitions/${getCompetition()}/relations`
|
||||
}
|
||||
239
src/50-common-ui.js
Normal file
239
src/50-common-ui.js
Normal file
@ -0,0 +1,239 @@
|
||||
function createPseudoButton(button, callback, clickAfter = true) {
|
||||
let pseudoButton = Tag.button({
|
||||
type: "button",
|
||||
textContent: button.value,
|
||||
classes: [...button.classList].join(" ") + " pseudo-save",
|
||||
on: {
|
||||
click: async () => {
|
||||
await callback()
|
||||
if (clickAfter) {
|
||||
button.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
button.style.display = "none"
|
||||
button.parentElement.insertBefore(pseudoButton, button)
|
||||
}
|
||||
|
||||
function makeDownloadLink(name, href = null) {
|
||||
return Tag.a({
|
||||
download: name,
|
||||
textContent: "Скачать",
|
||||
target: "_blank",
|
||||
href: href
|
||||
})
|
||||
}
|
||||
|
||||
function makeSwiper(dialog, fileListContainer, src) {
|
||||
let files = [...fileListContainer.querySelectorAll(`:is(.preview, .preview-small)`)]
|
||||
|
||||
let swiperWrapper = Tag.div({
|
||||
classes: "swiper-wrapper"
|
||||
})
|
||||
|
||||
for (const file of files) {
|
||||
let swiperSlide = Tag.div({
|
||||
classes: "swiper-slide",
|
||||
children: [
|
||||
Tag.a({
|
||||
classes: "swiper-download-link",
|
||||
href: file.dataset.origin,
|
||||
text: "Скачать",
|
||||
target: "_blank"
|
||||
}),
|
||||
Tag.img({
|
||||
src: file.dataset.origin
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
swiperWrapper.append(swiperSlide)
|
||||
}
|
||||
|
||||
let swiperDiv = Tag.div({
|
||||
classes: "swiper",
|
||||
children: [
|
||||
swiperWrapper,
|
||||
Tag.div({
|
||||
classes: "swiper-button-prev"
|
||||
}),
|
||||
Tag.div({
|
||||
classes: "swiper-button-next"
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
dialog.append(swiperDiv)
|
||||
|
||||
new Swiper('.swiper', {
|
||||
initialSlide: files.findIndex(el => el.dataset.origin && el.dataset.origin === src || el.src === src),
|
||||
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
},
|
||||
})
|
||||
|
||||
bindArrowsForGallery('.swiper-button-prev', '.swiper-button-next')
|
||||
}
|
||||
|
||||
const bindArrowsForGallery = (function () {
|
||||
let executed = false
|
||||
return function(leftButtonQuery, rightButtonQuery) {
|
||||
if (executed) return
|
||||
executed = true
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (!document.querySelector("dialog").open) return
|
||||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
document.querySelector(leftButtonQuery).click()
|
||||
break
|
||||
|
||||
case "ArrowRight":
|
||||
document.querySelector(rightButtonQuery).click()
|
||||
break
|
||||
|
||||
default: return
|
||||
}
|
||||
e.preventDefault()
|
||||
})
|
||||
}
|
||||
})()
|
||||
|
||||
function createMenuFromLinks(links, blanks = []) {
|
||||
let menu = Tag.make("menu")
|
||||
|
||||
for (const [label, href] of Object.entries(links)) {
|
||||
let menuItem = Tag.make("li")
|
||||
let link = Tag.a({
|
||||
href: href,
|
||||
innerText: label
|
||||
})
|
||||
|
||||
if (blanks.includes(label)) {
|
||||
link.target = "_blank"
|
||||
}
|
||||
|
||||
menuItem.append(link)
|
||||
menu.append(menuItem)
|
||||
}
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
function addStickyMenu() {
|
||||
let competition = getCompetition()
|
||||
if (!competition) return
|
||||
|
||||
let menuContainer = Tag.make("nav", {
|
||||
classes: "sticky-menu"
|
||||
})
|
||||
|
||||
let mainLinksFormatted = {}
|
||||
for (const [label, href] of Object.entries(links)) {
|
||||
mainLinksFormatted[label] = href.startsWith("http") ? href : `/${competition}/${href}`
|
||||
}
|
||||
menuContainer.append(createMenuFromLinks(mainLinksFormatted, ["Легенда"]))
|
||||
|
||||
let goToContainer = Tag.make("li", {
|
||||
classes: "go-to-container",
|
||||
children: [
|
||||
Tag.input({
|
||||
type: "text",
|
||||
id: "go-to-cp"
|
||||
}),
|
||||
Tag.span({
|
||||
textContent: "✏️ КП",
|
||||
on: {
|
||||
click: async function () {
|
||||
let number = document.querySelector("#go-to-cp").value
|
||||
let json = await getPointsByField("number", [number])
|
||||
let id = json[number].cp_id
|
||||
|
||||
if (id == null) {
|
||||
alert("Нет КП с таким номером")
|
||||
return
|
||||
}
|
||||
|
||||
location.href = cpLink(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
;[...menuContainer.querySelectorAll(`li`)].find(el => el.querySelector(`a`).textContent.trim() == "Контрольные пункты")?.after(goToContainer)
|
||||
|
||||
let catId = urlParams().get("cat_id")
|
||||
if (catId !== null) {
|
||||
let catLinksFormatted = {}
|
||||
for (const [label, href] of Object.entries(catLinks)) {
|
||||
catLinksFormatted[label] = href(catId)
|
||||
}
|
||||
|
||||
menuContainer.append(createMenuFromLinks(catLinksFormatted))
|
||||
}
|
||||
|
||||
document.querySelector("#header").after(menuContainer)
|
||||
|
||||
addStylesToHead(`
|
||||
.sticky-menu {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
|
||||
menu {
|
||||
display: flex;
|
||||
gap: .3rem;
|
||||
background: white;
|
||||
font-size: 1.2rem;
|
||||
padding-left: 10px;
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: " | "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.go-to-container {
|
||||
input {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: .5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function addClearBoth() {
|
||||
let clearEl = Tag.div()
|
||||
clearEl.style.clear = "both"
|
||||
document.body.append(clearEl)
|
||||
}
|
||||
|
||||
function makeDialog() {
|
||||
let dialog = Tag.make("dialog", {
|
||||
id: "dialog",
|
||||
on: {
|
||||
click: e => {
|
||||
if (e.target == dialog) {
|
||||
e.target.close()
|
||||
}
|
||||
},
|
||||
close: e => {
|
||||
e.target.innerHTML = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
document.body.append(dialog)
|
||||
}
|
||||
380
src/60-cp-files.js
Normal file
380
src/60-cp-files.js
Normal file
@ -0,0 +1,380 @@
|
||||
async function sendFileDeleted(inputName, fileInputVariant = null) {
|
||||
let formData = new FormData()
|
||||
|
||||
let normalInputName = fileInputVariant !== null ? fileInputs[inputName][fileInputVariant] : fileInputs[inputName]
|
||||
formData.set(`cp[id]`, document.querySelector("input[name='cp[id]']").value)
|
||||
formData.set(`cp[${normalInputName}]`, "-1")
|
||||
let result = await updatePoint(formData)
|
||||
try {
|
||||
console.log(result.ok)
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
function parsePurpose(cell) {
|
||||
return cell?.querySelector("td:nth-child(2)")?.innerHTML.match(/для[^\|]+/g)?.[0].trim()
|
||||
}
|
||||
|
||||
function parsefileInputVariant(cell) {
|
||||
return Object.keys(fileInpitVariants).find(key => fileInpitVariants[key] === parsePurpose(cell))
|
||||
}
|
||||
|
||||
function addDisplayedFile(inputName, type, file, preview, removeLink, removedFilesLinks, variant = null) {
|
||||
if (!inputName || !file || !removeLink) return
|
||||
|
||||
let fileListContainer
|
||||
if (variant !== null) {
|
||||
if (inputName == "attachment1" || inputName == "attachment4") {
|
||||
if (variant == "l") {
|
||||
fileListContainer = document.querySelector(`[data-container="${fileInputs[inputName][variant]}"] .file-list-container`)
|
||||
}
|
||||
else if (variant == "h") {
|
||||
let filesContainer = document.querySelector(`[data-container="${fileInputs[inputName].h}"]`)
|
||||
fileListContainer = getFileListContainer(filesContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let input = document.querySelector(`input[name="${inputName}\[\]"]`)
|
||||
fileListContainer ??= input.parentElement.parentElement.querySelector(".file-list-container")
|
||||
let fileContainer = Tag.div({
|
||||
classes: "file-container"
|
||||
})
|
||||
|
||||
const displayedFile = Tag.make(type ?? "a", {
|
||||
classes: "preview-small"
|
||||
})
|
||||
|
||||
if (type == "img") {
|
||||
displayedFile.src = preview
|
||||
displayedFile.dataset.origin = file
|
||||
displayedFile.addEventListener("click", () => {
|
||||
let dialog = document.querySelector("dialog")
|
||||
makeSwiper(dialog, fileListContainer, file)
|
||||
dialog.showModal()
|
||||
})
|
||||
}
|
||||
else if (type == "audio") {
|
||||
displayedFile.src = file
|
||||
displayedFile.setAttribute("controls", "")
|
||||
}
|
||||
else {
|
||||
displayedFile.href = file
|
||||
displayedFile.textContent = "Файл"
|
||||
}
|
||||
|
||||
let fileButtonsContainer = Tag.div({
|
||||
classes: "file-buttons-container"
|
||||
})
|
||||
|
||||
fileButtonsContainer.append(makeDownloadLink(/[^/]*$/.exec(new URL(file).pathname)[0], file))
|
||||
|
||||
let deleteButton = Tag.button({
|
||||
type: "button",
|
||||
classes: "button-delete",
|
||||
textContent: "x",
|
||||
on: {
|
||||
click: async e => {
|
||||
if (!confirm("Точно?")) return
|
||||
|
||||
await fetch(removeLink)
|
||||
await sendFileDeleted(inputName, variant)
|
||||
removedFilesLinks.push(removeLink)
|
||||
|
||||
fileListContainer.removeChild(fileContainer)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fileButtonsContainer.append(deleteButton)
|
||||
|
||||
fileContainer.append(displayedFile)
|
||||
fileContainer.append(fileButtonsContainer)
|
||||
|
||||
fileListContainer.append(fileContainer)
|
||||
}
|
||||
|
||||
function dataURLtoFile(dataurl, filename) {
|
||||
let arr = dataurl.split(',')
|
||||
let mime = arr[0].match(/:(.*?);/)[1]
|
||||
let bstr = atob(arr[arr.length - 1])
|
||||
let n = bstr.length
|
||||
let u8arr = new Uint8Array(n)
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n)
|
||||
}
|
||||
return new File([u8arr], filename, { type: mime })
|
||||
}
|
||||
|
||||
function getHtmlElementByFileType(file) {
|
||||
const htmlElementsForTypes = {
|
||||
"image/": "img",
|
||||
"audio/": "audio"
|
||||
}
|
||||
let res = "div"
|
||||
for (const [type, el] of Object.entries(htmlElementsForTypes)) {
|
||||
if (file.type.startsWith(type)) {
|
||||
res = el
|
||||
break
|
||||
}
|
||||
}
|
||||
let el = Tag.make(res)
|
||||
if (res == "audio") {
|
||||
el.setAttribute("controls", "")
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
function makeHandleFilesFunc() {
|
||||
let storedFiles = []
|
||||
|
||||
return function () {
|
||||
let dt = new DataTransfer()
|
||||
|
||||
if (storedFiles.length) {
|
||||
for (const oldFile of storedFiles) {
|
||||
let oldFileObj = dataURLtoFile(oldFile.data, oldFile.name)
|
||||
dt.items.add(oldFileObj)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
const file = this.files[i]
|
||||
dt.items.add(file)
|
||||
|
||||
let variant = this.parentElement.querySelector('input[type="radio"]:checked')?.value
|
||||
let fileListContainer
|
||||
|
||||
if (variant) {
|
||||
let [_, filesContainer] = getContainersByVariant(variant, getAttachmentIndex(this))
|
||||
fileListContainer = getFileListContainer(filesContainer)
|
||||
}
|
||||
else {
|
||||
fileListContainer = this.parentElement.parentElement.querySelector(".file-list-container")
|
||||
}
|
||||
|
||||
let fileContainer = Tag.div({
|
||||
classes: "file-container file-container-new"
|
||||
})
|
||||
|
||||
const displayedFile = getHtmlElementByFileType(file)
|
||||
displayedFile.classList.add("preview")
|
||||
displayedFile.file = file
|
||||
displayedFile.addEventListener("click", () => {
|
||||
let dialog = document.querySelector("dialog")
|
||||
makeSwiper(dialog, fileListContainer, displayedFile.src)
|
||||
dialog.showModal()
|
||||
})
|
||||
|
||||
let fileButtonsContainer = Tag.div({
|
||||
classes: "file-buttons-container"
|
||||
})
|
||||
|
||||
let downloadLink = makeDownloadLink(file.name)
|
||||
fileButtonsContainer.append(downloadLink)
|
||||
|
||||
let deleteButton = Tag.button({
|
||||
classes: "button-delete",
|
||||
textContent: "x",
|
||||
type: "button",
|
||||
on: {
|
||||
click: e => {
|
||||
let index = [...fileContainer.parentElement.children].indexOf(fileContainer)
|
||||
|
||||
if (!confirm("Точно?")) return
|
||||
|
||||
storedFiles.splice(storedFiles.length - this.files.length + index, 1)
|
||||
|
||||
let rdt = new DataTransfer()
|
||||
for (const file of this.files) {
|
||||
rdt.items.add(file)
|
||||
}
|
||||
rdt.items.remove(index)
|
||||
this.files = rdt.files
|
||||
|
||||
fileContainer.parentElement.removeChild(fileContainer)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fileButtonsContainer.append(deleteButton)
|
||||
|
||||
fileContainer.append(displayedFile)
|
||||
fileContainer.append(fileButtonsContainer)
|
||||
|
||||
fileListContainer.append(fileContainer)
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
displayedFile.src = e.target.result
|
||||
downloadLink.setAttribute("href", displayedFile.src)
|
||||
|
||||
storedFiles.push({ data: displayedFile.src, name: file.name })
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
this.files = dt.files
|
||||
}
|
||||
}
|
||||
|
||||
let prettifyFiles = (function () {
|
||||
let executed = false
|
||||
return function (insertedFileRows, removedFilesLinks) {
|
||||
if (executed) return
|
||||
executed = true
|
||||
|
||||
document.querySelector("#new").querySelectorAll("input[type=file]").forEach((element, index) => {
|
||||
element.id = `input-file-${index}`
|
||||
element.setAttribute("multiple", "multiple")
|
||||
element.dataset.index = index
|
||||
let pseudoInput = Tag.make("label", {
|
||||
classes: "custom-file-upload",
|
||||
_for: element.id,
|
||||
textContent: "+"
|
||||
})
|
||||
element.parentElement.insertBefore(pseudoInput, element)
|
||||
|
||||
element.parentElement.parentElement.append(makeFileListContainer())
|
||||
|
||||
element.addEventListener("change", makeHandleFilesFunc(), false)
|
||||
})
|
||||
|
||||
moveFileInputValues(insertedFileRows, removedFilesLinks)
|
||||
}
|
||||
})()
|
||||
|
||||
function saveInputValues(from) {
|
||||
let fromInputs = [...from.querySelectorAll("input, textarea")]
|
||||
let result = new Map()
|
||||
|
||||
for (const fromEl of fromInputs) {
|
||||
result.set(fromEl, fromEl.type == "radio" ? fromEl.checked : fromEl.value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function setInputValues(to, values) {
|
||||
let toInputs = [...to.querySelectorAll("input, textarea")]
|
||||
|
||||
for (const [fromEl, value] of values) {
|
||||
let currentToInput = toInputs.find(toEl =>
|
||||
toEl.type !== 'file' &&
|
||||
toEl.type === fromEl.type &&
|
||||
toEl.name === fromEl.name &&
|
||||
toEl.id === fromEl.id &&
|
||||
(toEl.type !== "radio" || toEl.value === fromEl.value)
|
||||
)
|
||||
|
||||
if (currentToInput) {
|
||||
currentToInput.checked = fromEl.checked
|
||||
currentToInput.value = fromEl.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveFileInputValues(insertedFileRows, removedFilesLinks) {
|
||||
for (const [inputRowIndex, rows] of insertedFileRows) {
|
||||
for (const row of rows) {
|
||||
let fileInput = null, type = null, fileUrl = null, deleteUrl = null, preview = null
|
||||
if (row.querySelector("img")) {
|
||||
type = "img"
|
||||
fileUrl = row.querySelector("td:first-child a").href
|
||||
preview = row.querySelector("img").src
|
||||
}
|
||||
else if (row.querySelector("audio")) {
|
||||
type = "audio"
|
||||
fileUrl = row.querySelector("audio").src
|
||||
}
|
||||
else {
|
||||
fileUrl = row.querySelector("td:first-child a").href
|
||||
}
|
||||
|
||||
deleteUrl = row.querySelector("td:nth-child(2) a").href
|
||||
|
||||
if (inputRowIndex == 23) {
|
||||
fileInput = "attachment1"
|
||||
}
|
||||
else if (inputRowIndex == 27) {
|
||||
fileInput = "attachment4"
|
||||
}
|
||||
else if (inputRowIndex == 31) {
|
||||
fileInput = "attachment2"
|
||||
}
|
||||
else if (inputRowIndex == 35) {
|
||||
fileInput = "attachment3"
|
||||
}
|
||||
else {
|
||||
console.log(inputRowIndex)
|
||||
return
|
||||
}
|
||||
|
||||
let variant = parsefileInputVariant(row)
|
||||
|
||||
addDisplayedFile(fileInput, type, fileUrl, preview, deleteUrl, removedFilesLinks, variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveNewFilesOnVariantChange() {
|
||||
document.querySelectorAll(`input[name^="new_file_type"]`).forEach(el => el.addEventListener("change", function () {
|
||||
let attachmentIndex = getAttachmentIndex(this)
|
||||
|
||||
let [from, to] = getContainersByVariant(this.value, attachmentIndex)
|
||||
let newFiles = [...from.querySelectorAll(`.file-container-new`)]
|
||||
for (const newFile of newFiles) {
|
||||
getFileListContainer(to).append(newFile)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function removeDeletedFilesFromUglyVersion(removeLink) {
|
||||
let shortHref = removeLink.split("/").at(-1)
|
||||
document.querySelector(`tr:has(a[href="${shortHref}"])`).remove()
|
||||
}
|
||||
|
||||
function moveInputValues(form, was, became) {
|
||||
let inputValues = saveInputValues(was)
|
||||
form.removeChild(was)
|
||||
form.append(became)
|
||||
setInputValues(became, inputValues)
|
||||
createSendButtons()
|
||||
}
|
||||
|
||||
function prettifyEditCpForm(form, was, became, insertedFileRows, removedFilesLinks) {
|
||||
moveInputValues(form, was, became)
|
||||
prettifyFiles(insertedFileRows, removedFilesLinks)
|
||||
|
||||
moveNewFilesOnVariantChange()
|
||||
$("#cps_main").select2()
|
||||
makeCoordinatesLinks()
|
||||
}
|
||||
|
||||
function uglifyEditCpForm(form, was, became, removedFilesLinks) {
|
||||
moveInputValues(form, was, became)
|
||||
for (const removedFileLink of removedFilesLinks) {
|
||||
removeDeletedFilesFromUglyVersion(removedFileLink)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
1012
src/70-cp-edit.js
Normal file
1012
src/70-cp-edit.js
Normal file
File diff suppressed because it is too large
Load Diff
44
src/75-map.js
Normal file
44
src/75-map.js
Normal file
@ -0,0 +1,44 @@
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
257
src/80-route-build.js
Normal file
257
src/80-route-build.js
Normal file
@ -0,0 +1,257 @@
|
||||
function prettifyRouteBuildPage() {
|
||||
function changeColumnWidth() {
|
||||
let styles = `
|
||||
#content table table {
|
||||
td:nth-child(2) {
|
||||
width: 10%;
|
||||
|
||||
select {
|
||||
width: 5em !important;
|
||||
}
|
||||
}
|
||||
|
||||
td:nth-child(3) {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
td:nth-child(4) {
|
||||
width: 12%;
|
||||
|
||||
input {
|
||||
width: 4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
addStylesToHead(styles)
|
||||
}
|
||||
|
||||
function hideDescription() {
|
||||
document.querySelectorAll(`#content > form > table > tbody > tr:is(:nth-child(3), :nth-child(6)) `).forEach(el => el.remove())
|
||||
}
|
||||
|
||||
function hideStartRow() {
|
||||
document.querySelector(`#content table table tr:nth-child(2) `).remove()
|
||||
}
|
||||
|
||||
function getRows() {
|
||||
return [...document.querySelectorAll(`#content > form > table > tbody > tr:nth-child(3) tr`)]
|
||||
}
|
||||
|
||||
function useColspanForFinishWarning() {
|
||||
let rows = getRows()
|
||||
let finishRow = rows.find(el => el.querySelector(`td`)?.textContent.trim() == "Финиш маршрута")
|
||||
|
||||
finishRow.querySelectorAll(`td:not(:nth-child(2))`).forEach(el => el.remove())
|
||||
finishRow.querySelector(`td`).colSpan = 4
|
||||
}
|
||||
|
||||
function makeStatsSpan(text) {
|
||||
let statsSpan = Tag.span({
|
||||
textContent: text,
|
||||
classes: "stats"
|
||||
})
|
||||
|
||||
return statsSpan
|
||||
}
|
||||
|
||||
function getPointCells() {
|
||||
return [...document.querySelectorAll(`#props table td:first-child`)]
|
||||
}
|
||||
|
||||
function getPointNumberFromCell(cell) {
|
||||
return cell.textContent.trim().match(/^\d+/)?.[0]
|
||||
}
|
||||
|
||||
function getPointNumbers() {
|
||||
let pointNumbers = {}
|
||||
for (const [rowIndex, pointCell] of getPointCells().entries()) {
|
||||
let pointNumber = getPointNumberFromCell(pointCell)
|
||||
if (pointNumber != null)
|
||||
pointNumbers[rowIndex] = pointNumber
|
||||
}
|
||||
|
||||
return pointNumbers
|
||||
}
|
||||
|
||||
async function getPointsFromRows() {
|
||||
return await getPointsByField("number", Object.values(getPointNumbers()))
|
||||
}
|
||||
|
||||
function countCp(points) {
|
||||
let rows = getRows()
|
||||
|
||||
let fullCount = 0
|
||||
let stageCount = 0
|
||||
let fullPuzzleCount = 0
|
||||
let stagePuzzleCount = 0
|
||||
let fullNeedHistoryCount = 0
|
||||
let stageNeedHistoryCount = 0
|
||||
let fullHistoryCount = 0
|
||||
let stageHistoryCount = 0
|
||||
|
||||
let stageRow = null
|
||||
for (const [i, row] of rows.entries()) {
|
||||
if (i < 2) continue
|
||||
let isFinish = row.querySelector(".attention") !== null
|
||||
|
||||
if (isFinish || row.querySelector(`th`)) {
|
||||
if (stageRow) {
|
||||
let stageHeader = stageRow.querySelector("th")
|
||||
stageHeader.append(makeStatsSpan(`КП в этапе: ${stageCount}`))
|
||||
stageHeader.append(makeStatsSpan(`Загадок: ${stagePuzzleCount}`))
|
||||
stageHeader.append(makeStatsSpan(`Нужно ИС: ${stageNeedHistoryCount}`))
|
||||
stageHeader.append(makeStatsSpan(`ИС: ${stageHistoryCount}`))
|
||||
}
|
||||
|
||||
if (isFinish) break
|
||||
|
||||
stageRow = row
|
||||
stageCount = 0
|
||||
stagePuzzleCount = 0
|
||||
stageNeedHistoryCount = 0
|
||||
stageHistoryCount = 0
|
||||
continue
|
||||
}
|
||||
|
||||
let pointNumber = getPointNumberFromCell(row.querySelector("td"))
|
||||
if (!pointNumber) continue
|
||||
|
||||
fullCount++
|
||||
stageCount++
|
||||
|
||||
let point = points[pointNumber]
|
||||
if (!point) continue
|
||||
|
||||
if (point.is_puzzle) {
|
||||
fullPuzzleCount++
|
||||
stagePuzzleCount++
|
||||
}
|
||||
|
||||
if (point.history_needed) {
|
||||
fullNeedHistoryCount++
|
||||
stageNeedHistoryCount++
|
||||
}
|
||||
|
||||
if (point.history_ru) {
|
||||
fullHistoryCount++
|
||||
stageHistoryCount++
|
||||
}
|
||||
}
|
||||
|
||||
let tableHeader = document.querySelector("table tr:nth-child(2) th")
|
||||
tableHeader.append(makeStatsSpan(`Всего КП: ${fullCount}`))
|
||||
tableHeader.append(makeStatsSpan(`Загадок: ${fullPuzzleCount}`))
|
||||
tableHeader.append(makeStatsSpan(`Нужно ИС: ${fullNeedHistoryCount}`))
|
||||
tableHeader.append(makeStatsSpan(`ИС: ${fullHistoryCount}`))
|
||||
|
||||
addStylesToHead(`
|
||||
.stats {
|
||||
color: red;
|
||||
margin-left: .5em;
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function addLinksToCp(points) {
|
||||
let pointCells = getPointCells()
|
||||
|
||||
for (const [i, pointCell] of pointCells.entries()) {
|
||||
let pointNumber = getPointNumberFromCell(pointCell)
|
||||
if (pointNumber == null) continue
|
||||
|
||||
let cellContent = pointCell.textContent
|
||||
pointCell.innerHTML = ''
|
||||
pointCell.append(Tag.a({
|
||||
href: cpLink(points[pointNumber].cp_id),
|
||||
target: "_blank",
|
||||
textContent: cellContent
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function showHistoryData(points) {
|
||||
let pointCells = getPointCells()
|
||||
|
||||
for (const [i, pointCell] of pointCells.entries()) {
|
||||
let pointNumber = getPointNumberFromCell(pointCell)
|
||||
if (pointNumber == null) continue
|
||||
|
||||
let point = points[pointNumber]
|
||||
|
||||
if (point.history_needed) {
|
||||
pointCell.append(Tag.span({ classes: "point-info", textContent: "Нужна ИС" }))
|
||||
}
|
||||
|
||||
if (point.history_ru) {
|
||||
pointCell.append(Tag.span({ classes: "point-info", textContent: "Есть ИС" }))
|
||||
}
|
||||
|
||||
if (point.photo_answer) {
|
||||
pointCell.append(Tag.span({ classes: "point-info", textContent: "Селфи" }))
|
||||
}
|
||||
}
|
||||
|
||||
addStylesToHead(`
|
||||
.point-info {
|
||||
&:first-of-type {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
& + &::before {
|
||||
content: " | "
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function getRouteName() {
|
||||
return document.querySelector(`#content h1`).textContent.match(/(?<=\")[^\"]+/)
|
||||
}
|
||||
|
||||
function sendRouteData() {
|
||||
createPseudoButton(document.querySelector(`input[name="save_sort"]`), async () => {
|
||||
let formData = new FormData(document.querySelector(`form[name="main_form"]`))
|
||||
|
||||
let catId = urlParams().get("cat_id")
|
||||
formData.set("cat_id", catId)
|
||||
formData.set("cat_name", getRouteName())
|
||||
await updateRoute(formData)
|
||||
})
|
||||
}
|
||||
|
||||
function showUnknownPoints() {
|
||||
(async () => {
|
||||
let unknownPoints = await getUnknownPoints()
|
||||
if (unknownPoints.error || !unknownPoints.length) return
|
||||
|
||||
let unknownPointsContainer = Tag.div()
|
||||
unknownPointsContainer.append("Несохраненные точки: ")
|
||||
for (const [i, cpId] of unknownPoints.entries()) {
|
||||
if (i !== 0) {
|
||||
unknownPointsContainer.append(", ")
|
||||
}
|
||||
|
||||
unknownPointsContainer.append(Tag.a({
|
||||
href: cpLink(cpId),
|
||||
textContent: cpId
|
||||
}))
|
||||
}
|
||||
document.querySelector(`#content h1`).after(unknownPointsContainer)
|
||||
})()
|
||||
}
|
||||
|
||||
addJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js")
|
||||
changeColumnWidth()
|
||||
hideDescription()
|
||||
hideStartRow()
|
||||
useColspanForFinishWarning()
|
||||
;(async () => {
|
||||
let points = await getPointsFromRows()
|
||||
countCp(points)
|
||||
addLinksToCp(points)
|
||||
showHistoryData(points)
|
||||
})()
|
||||
sendRouteData()
|
||||
showUnknownPoints()
|
||||
}
|
||||
168
src/90-route-edit.js
Normal file
168
src/90-route-edit.js
Normal file
@ -0,0 +1,168 @@
|
||||
function getCpNumberFromOption(option) {
|
||||
return option.textContent.match(/(?<=^\s*#)\d+/g)?.[0]
|
||||
}
|
||||
|
||||
function matchNumberFromSelect(select) {
|
||||
return [...document.querySelector(select).options].reduce(
|
||||
(res, el) => ({ ...res, [getCpNumberFromOption(el)]: el.value }),
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
function unselectAll(select) {
|
||||
[...document.querySelector(select).selectedOptions].forEach(el => el.selected = false)
|
||||
}
|
||||
|
||||
function scrollSelectToBottom(select) {
|
||||
document.querySelector(`${select} option:last-child`).scrollIntoView()
|
||||
}
|
||||
|
||||
function prettifyRouteEditPage() {
|
||||
function createPointsInpit() {
|
||||
let pointInput = Tag.input({
|
||||
id: "add-point",
|
||||
type: "text",
|
||||
on: {
|
||||
focus: () => {
|
||||
pointInput.classList.remove("success", "error")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let addButton = Tag.button({
|
||||
type: "button",
|
||||
textContent: "Добавить",
|
||||
id: "add-button",
|
||||
on: {
|
||||
click: () => {
|
||||
let point = pointInput.value
|
||||
if (point == "") return
|
||||
|
||||
let availablePoints = matchNumberFromSelect("#cps_avail")
|
||||
let addedPoints = matchNumberFromSelect("#cps_in")
|
||||
|
||||
if (point in addedPoints) {
|
||||
alert("Точка уже добавлена")
|
||||
return
|
||||
}
|
||||
|
||||
if (!(point in availablePoints)) {
|
||||
alert("Точка не найдена")
|
||||
return
|
||||
}
|
||||
|
||||
unselectAll("#cps_avail")
|
||||
let option = document.querySelector(`#cps_avail option[value="${availablePoints[point]}"]`)
|
||||
option.selected = true
|
||||
document.querySelector(`#cps_add`).click()
|
||||
pointInput.classList.add("success")
|
||||
pointInput.value = ""
|
||||
option.selected = false
|
||||
scrollSelectToBottom("#cps_in")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let removeButton = Tag.button({
|
||||
type: "button",
|
||||
textContent: "Убрать",
|
||||
on: {
|
||||
click: () => {
|
||||
let point = pointInput.value
|
||||
if (point == "") return
|
||||
|
||||
let addedPoints = matchNumberFromSelect("#cps_in")
|
||||
|
||||
if (!(point in addedPoints)) return
|
||||
|
||||
unselectAll("#cps_in")
|
||||
let option = document.querySelector(`#cps_in option[value="${addedPoints[point]}"]`)
|
||||
option.selected = true
|
||||
document.querySelector(`#cps_delete`).click()
|
||||
pointInput.classList.add("error")
|
||||
pointInput.value = ""
|
||||
option.selected = false
|
||||
scrollSelectToBottom("#cps_in")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let addPointsContainer = Tag.div({
|
||||
children: [
|
||||
addButton,
|
||||
pointInput,
|
||||
removeButton
|
||||
]
|
||||
})
|
||||
document.querySelector("table tr:nth-child(16) td").prepend(addPointsContainer)
|
||||
|
||||
addStylesToHead(`
|
||||
#cps_in, #cps_avail {
|
||||
height: 15em;
|
||||
}
|
||||
|
||||
.success {
|
||||
border-color: green;
|
||||
}
|
||||
|
||||
.error {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
#add-point {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
#add-button {
|
||||
margin-left: 420px;
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function makeTextareasOneRow() {
|
||||
document.querySelectorAll(`textarea:is([name="track[comment_int]"], [name="track[comment_ext]"])`).forEach(el => {
|
||||
el.rows = 1
|
||||
})
|
||||
}
|
||||
|
||||
function hideRedundantRows() {
|
||||
document.querySelectorAll(`table tr:is(:nth-child(3), :nth-child(4), :nth-child(5))`).forEach(el => el.classList.add("hidden"))
|
||||
}
|
||||
|
||||
async function getPointsFromSelect() {
|
||||
let options = [...document.querySelectorAll(`#cps_in option`)]
|
||||
return await getPointsByField("number", options.map(option => getCpNumberFromOption(option)))
|
||||
}
|
||||
|
||||
function paintCpList(points) {
|
||||
let options = [...document.querySelectorAll(`#cps_in option`)]
|
||||
let catId = urlParams().get("cat_id")
|
||||
|
||||
for (const option of options) {
|
||||
let category = points[getCpNumberFromOption(option)].categories.find(category => category.cat_id === catId)
|
||||
if (category && category.pivot.bonus_time == "0") {
|
||||
let stage = parseInt(category.pivot.stage)
|
||||
option.style['background-color'] = stageColors[(stage - 1) % stageColors.length]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideAlreadyAddedPoints(points) {
|
||||
let options = [...document.querySelectorAll(`#cps_avail option`)]
|
||||
|
||||
for (const option of options) {
|
||||
if (getCpNumberFromOption(option) in points) {
|
||||
option.style.display = "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeTextareasOneRow()
|
||||
hideRedundantRows()
|
||||
createPointsInpit()
|
||||
;(async () => {
|
||||
let points = await getPointsFromSelect()
|
||||
paintCpList(points)
|
||||
hideAlreadyAddedPoints(points)
|
||||
})()
|
||||
}
|
||||
19
tpl.js
Normal file
19
tpl.js
Normal file
@ -0,0 +1,19 @@
|
||||
// ==UserScript==
|
||||
// @name Runcity
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 2026-08-15
|
||||
// @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';
|
||||
|
||||
code;
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user