Tags: Mobile edit Mobile web edit Advanced mobile edit |
Tags: Mobile edit Mobile web edit Advanced mobile edit |
(53 intermediate revisions by the same user not shown) |
Line 1: |
Line 1: |
| (function () { | | (function () { |
| const iconURL = "https://files.catbox.moe/wk78nl.jpg"; | | function constrainPosition(x, y, width, height) { |
| | const winWidth = window.innerWidth; |
| | const winHeight = window.innerHeight; |
| | if (x < 0) x = 0; |
| | if (y < 0) y = 0; |
| | if (x + width > winWidth) x = winWidth - width; |
| | if (y + height > winHeight) y = winHeight - height; |
| | return [x, y]; |
| | } |
| | |
| | const toggle = document.createElement("button"); |
| | toggle.id = "delta-toggle"; |
| | Object.assign(toggle.style, { |
| | position: "fixed", |
| | bottom: "20px", |
| | right: "20px", |
| | padding: "0", |
| | background: "#2b2b2b", |
| | border: "none", |
| | borderRadius: "12px", |
| | zIndex: 9998, |
| | cursor: "move", |
| | userSelect: "none", |
| | touchAction: "none", |
| | }); |
| | |
| | const icon = document.createElement("img"); |
| | icon.src = "https://files.catbox.moe/uodscw.png"; |
| | icon.alt = "Delta"; |
| | Object.assign(icon.style, { |
| | width: "48px", |
| | height: "48px", |
| | borderRadius: "12px", |
| | display: "block", |
| | pointerEvents: "none", |
| | }); |
| | toggle.appendChild(icon); |
|
| |
|
| // Create menu
| |
| const menu = document.createElement("div"); | | const menu = document.createElement("div"); |
| menu.id = "mahitoMenu"; | | menu.id = "delta-menu"; |
| Object.assign(menu.style, { | | Object.assign(menu.style, { |
| | display: "none", |
| position: "fixed", | | position: "fixed", |
| top: "20px", | | bottom: "80px", |
| right: "20px", | | right: "20px", |
| padding: "10px", | | width: "300px", |
| backgroundColor: "#111", | | background: "#2b2b2b", |
| color: "#fff", | | color: "#fff", |
| border: "2px solid #666",
| |
| zIndex: "9999",
| |
| fontFamily: "monospace",
| |
| borderRadius: "10px", | | borderRadius: "10px", |
| | padding: "15px", |
| boxShadow: "0 0 10px rgba(0,0,0,0.5)", | | boxShadow: "0 0 10px rgba(0,0,0,0.5)", |
| | zIndex: 9999, |
| | fontFamily: "monospace", |
| | cursor: "move", |
| userSelect: "none", | | userSelect: "none", |
| touchAction: "none", | | touchAction: "none", |
| }); | | }); |
|
| |
|
| // Header
| | const title = document.createElement("div"); |
| const header = document.createElement("div"); | | title.textContent = "Delta Online"; |
| Object.assign(header.style, { | | Object.assign(title.style, { |
| cursor: "grab", | | textAlign: "center", |
| display: "flex", | | fontSize: "18px", |
| justifyContent: "center", | | marginBottom: "10px", |
| alignItems: "center", | | userSelect: "text", |
| marginBottom: "8px", | | cursor: "default", |
| gap: "6px",
| |
| }); | | }); |
|
| |
|
| // Left icon
| | const textarea = document.createElement("textarea"); |
| const leftIcon = document.createElement("img"); | | textarea.placeholder = "Enter JavaScript..."; |
| leftIcon.src = iconURL; | | Object.assign(textarea.style, { |
| leftIcon.width = 15;
| | width: "100%", |
| leftIcon.height = 15;
| | height: "120px", |
| leftIcon.alt = "Mahito icon";
| | background: "#1e1e1e", |
| leftIcon.style.borderRadius = "3px"; | |
| | |
| // Title text
| |
| const title = document.createElement("div");
| |
| title.textContent = "Mahito Menu";
| |
| title.style.fontWeight = "bold";
| |
| | |
| // Right icon
| |
| const rightIcon = document.createElement("img");
| |
| rightIcon.src = iconURL;
| |
| rightIcon.width = 15;
| |
| rightIcon.height = 15;
| |
| rightIcon.alt = "Mahito icon";
| |
| rightIcon.style.borderRadius = "3px";
| |
| | |
| header.appendChild(leftIcon);
| |
| header.appendChild(title);
| |
| header.appendChild(rightIcon);
| |
| | |
| menu.appendChild(header);
| |
| | |
| // Content area
| |
| const content = document.createElement("div");
| |
| | |
| // Mass edit button
| |
| const massEditBtn = document.createElement("button");
| |
| massEditBtn.textContent = "Mass edit";
| |
| massEditBtn.title = "It allows you to edit any line on any page(s)";
| |
| Object.assign(massEditBtn.style, {
| |
| background: "#555", | |
| color: "#fff", | | color: "#fff", |
| border: "none", | | border: "none", |
| padding: "5px 10px", | | borderRadius: "6px", |
| cursor: "pointer", | | padding: "10px", |
| marginBottom: "6px", | | resize: "none", |
| width: "100%", | | boxSizing: "border-box", |
| textAlign: "left", | | cursor: "auto", |
| | userSelect: "text", |
| }); | | }); |
|
| |
|
| // Open Explanation button
| | const runBtn = document.createElement("button"); |
| const openBtn = document.createElement("button"); | | runBtn.textContent = "Run Code"; |
| openBtn.textContent = "Open Explanation"; | | Object.assign(runBtn.style, { |
| Object.assign(openBtn.style, { | | marginTop: "10px", |
| background: "#444", | | width: "100%", |
| | padding: "10px", |
| | background: "#4caf50", |
| color: "#fff", | | color: "#fff", |
| border: "none", | | border: "none", |
| padding: "5px 10px", | | borderRadius: "6px", |
| cursor: "pointer", | | cursor: "pointer", |
| width: "100%",
| |
| textAlign: "left",
| |
| }); | | }); |
| openBtn.onclick = () => console.log("Open Explanation clicked");
| |
|
| |
|
| content.appendChild(massEditBtn); | | runBtn.onclick = () => { |
| content.appendChild(openBtn);
| | try { |
| menu.appendChild(content);
| | eval(textarea.value); |
| document.body.appendChild(menu);
| | } catch (e) { |
| | | alert("Error: " + e.message); |
| // Modal overlay & form for mass edit
| | } |
| const modal = document.createElement("div");
| |
| Object.assign(modal.style, {
| |
| display: "none", | |
| position: "fixed",
| |
| top: 0,
| |
| left: 0,
| |
| width: "100vw",
| |
| height: "100vh",
| |
| backgroundColor: "rgba(0,0,0,0.8)",
| |
| zIndex: "10000", | |
| justifyContent: "center",
| |
| alignItems: "center",
| |
| });
| |
| | |
| const form = document.createElement("form");
| |
| Object.assign(form.style, {
| |
| backgroundColor: "#222",
| |
| padding: "20px",
| |
| borderRadius: "8px",
| |
| color: "#fff",
| |
| maxWidth: "400px",
| |
| width: "90%",
| |
| display: "flex",
| |
| flexDirection: "column",
| |
| gap: "10px",
| |
| });
| |
| | |
| form.innerHTML = `
| |
| <label>Pages to edit (one per line):</label>
| |
| <textarea id="massEditPages" rows="5" style="width:100%;"></textarea>
| |
| | |
| <label>Text 1:</label>
| |
| <textarea id="massEditText1" rows="3" style="width:100%;"></textarea>
| |
| | |
| <label>Text 2:</label>
| |
| <textarea id="massEditText2" rows="3" style="width:100%;"></textarea>
| |
| | |
| <label>Edit type:</label>
| |
| <select id="massEditType" style="width:100%;">
| |
| <option value="prepend">Prepend text 1</option>
| |
| <option value="append">Append text 1</option>
| |
| <option value="bothpend">Prepend text 1 and append text 2</option>
| |
| <option value="replacetext">Replace first instance of text 1 with text 2</option>
| |
| <option value="replacetextg">Replace all instances of text 1 with text 2</option>
| |
| <option value="replacepage">Replace page with text 1</option>
| |
| </select>
| |
| | |
| <label>Edit summary:</label>
| |
| <input id="massEditSummary" type="text" style="width:100%;" />
| |
| | |
| <label><input id="massEditMinor" type="checkbox" /> Mark edit as minor</label>
| |
| | |
| <button type="submit" style="padding:8px; background:#555; border:none; color:#fff; cursor:pointer;">
| |
| Submit Mass Edit
| |
| </button>
| |
| <button type="button" id="massEditCancel" style="padding:8px; background:#333; border:none; color:#fff; cursor:pointer;">
| |
| Cancel
| |
| </button>
| |
| | |
| <div id="massEditResult" style="margin-top:10px;"></div>
| |
| `;
| |
| | |
| modal.appendChild(form);
| |
| document.body.appendChild(modal);
| |
| | |
| massEditBtn.onclick = () => {
| |
| modal.style.display = "flex"; | |
| }; | | }; |
|
| |
|
| document.getElementById("massEditCancel").onclick = (e) => { | | function makeDraggable(element, excludeElements = [], onClick) { |
| e.preventDefault(); | | let isDragging = false; |
| modal.style.display = "none"; | | let dragStartX = 0; |
| document.getElementById("massEditResult").textContent = ""; | | let dragStartY = 0; |
| };
| | let elemStartLeft = 0; |
| | let elemStartTop = 0; |
| | let moved = false; |
|
| |
|
| // Mass Edit function (your code slightly adapted for async)
| | function onDragStart(x, y, target) { |
| async function doMassEdit(pages, text1, text2, editType, summary, minor) {
| | if (excludeElements.includes(target)) return false; |
| let edited = 0;
| | isDragging = true; |
| const failed = [];
| | moved = false; |
| const errors = [];
| | dragStartX = x; |
| | | dragStartY = y; |
| function getPageText(title) {
| | const rect = element.getBoundingClientRect(); |
| return fetch( | | elemStartLeft = rect.left; |
| mw.config.get("wgScriptPath") +
| | elemStartTop = rect.top; |
| "/api.php?action=query&prop=revisions&rvprop=content&format=json&indexpageids=1&titles=" +
| | return true; |
| encodeURIComponent(title)
| |
| ) | |
| .then((res) => res.json())
| |
| .then((response) => {
| |
| const pageid = response.query.pageids[0];
| |
| if (pageid === "-1") return "";
| |
| return response.query.pages[pageid].revisions[0]["*"];
| |
| });
| |
| } | | } |
|
| |
|
| for (let article of pages) { | | function onDragMove(x, y) { |
| if (!article.trim()) continue; | | if (!isDragging) return; |
| | const dx = x - dragStartX; |
| | const dy = y - dragStartY; |
| | if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; |
|
| |
|
| try { | | let newLeft = elemStartLeft + dx; |
| // Get token | | let newTop = elemStartTop + dy; |
| const tokenResp = await fetch( | | [newLeft, newTop] = constrainPosition( |
| mw.config.get("wgScriptPath") +
| | newLeft, |
| "/api.php?format=json&action=query&prop=info&meta=tokens&type=csrf&titles=" +
| | newTop, |
| encodeURIComponent(article)
| | element.offsetWidth, |
| );
| | element.offsetHeight |
| const tokenJson = await tokenResp.json();
| | ); |
| const edittoken = tokenJson.query.tokens.csrftoken;
| | element.style.left = newLeft + "px"; |
| | element.style.top = newTop + "px"; |
| | element.style.bottom = "auto"; |
| | element.style.right = "auto"; |
| | } |
|
| |
|
| // Get original page text if needed
| | function onDragEnd() { |
| let newPageText = "";
| | if (!isDragging) return; |
| if (
| | isDragging = false; |
| editType === "replacetext" ||
| | if (!moved && typeof onClick === "function") { |
| editType === "replacetextg" ||
| | onClick(); |
| editType === "bothpend"
| |
| ) {
| |
| let pagetext = await getPageText(article);
| |
| if (editType === "replacetextg") {
| |
| const escapedText1 = text1.replace(/[.*+?|(){}\/g, "\\$&");
| |
| const reg = new RegExp(escapedText1, "g");
| |
| pagetext = pagetext.replace(reg, text2);
| |
| } else if (editType === "replacetext") {
| |
| pagetext = pagetext.replace(text1, text2);
| |
| } else {
| |
| pagetext = text1 + pagetext + text2;
| |
| }
| |
| newPageText = pagetext;
| |
| }
| |
| | |
| let postData = new URLSearchParams();
| |
| postData.append("format", "json");
| |
| postData.append("action", "edit");
| |
| postData.append("watchlist", "nochange");
| |
| postData.append("title", article);
| |
| postData.append("summary", summary);
| |
| postData.append("token", edittoken);
| |
| if (minor) postData.append("minor", "1");
| |
| else postData.append("notminor", "1");
| |
| | |
| if (editType === "prepend") {
| |
| postData.append("prependtext", text1 + "\n");
| |
| } else if (editType === "append") {
| |
| postData.append("appendtext", "\n" + text1);
| |
| } else if (editType === "replacepage") {
| |
| postData.append("text", text1);
| |
| } else if (
| |
| editType === "replacetext" ||
| |
| editType === "replacetextg" ||
| |
| editType === "bothpend"
| |
| ) {
| |
| postData.append("text", newPageText);
| |
| }
| |
| | |
| const editResp = await fetch(mw.config.get("wgScriptPath") + "/api.php", {
| |
| method: "POST",
| |
| headers: {
| |
| "Content-Type": "application/x-www-form-urlencoded",
| |
| },
| |
| body: postData.toString(),
| |
| });
| |
| | |
| const editJson = await editResp.json();
| |
| | |
| if (editJson.edit) {
| |
| edited++;
| |
| document.getElementById("massEditResult").textContent =
| |
| `Edited ${edited} pages...`;
| |
| } else {
| |
| failed.push(article);
| |
| errors.push(editJson.error?.info || "Unknown error");
| |
| }
| |
| } catch (err) {
| |
| failed.push(article); | |
| errors.push(err.message);
| |
| } | | } |
| } | | } |
|
| |
|
| // Show summary | | element.addEventListener("mousedown", (e) => { |
| if (failed.length > 0) {
| | if (!onDragStart(e.clientX, e.clientY, e.target)) return; |
| document.getElementById("massEditResult").innerHTML =
| | e.preventDefault(); |
| `<b>Done with errors.</b><br>Edited: ${edited}<br>Failed:` +
| | }); |
| failed
| | document.addEventListener("mousemove", (e) => onDragMove(e.clientX, e.clientY)); |
| .map(
| | document.addEventListener("mouseup", onDragEnd); |
| (page, i) =>
| |
| `<div>${page}: ${errors[i]}</div>`
| |
| )
| |
| .join("");
| |
| } else { | |
| document.getElementById("massEditResult").innerHTML =
| |
| `<b>All done!</b><br>Edited: ${edited}`;
| |
| } | |
| }
| |
|
| |
|
| // Handle form submit
| | element.addEventListener("touchstart", (e) => { |
| form.onsubmit = async (e) => {
| | if (!e.touches || !e.touches[0]) return; |
| e.preventDefault();
| | if (!onDragStart(e.touches[0].clientX, e.touches[0].clientY, e.target)) return; |
| document.getElementById("massEditResult").textContent = "Working...";
| | e.preventDefault(); |
| const pages = document
| | }); |
| .getElementById("massEditPages") | | document.addEventListener( |
| .value.split("\n")
| | "touchmove", |
| .map((x) => x.trim())
| | (e) => { |
| .filter((x) => x); | | if (!e.touches || !e.touches[0]) return; |
| const text1 = document.getElementById("massEditText1").value; | | onDragMove(e.touches[0].clientX, e.touches[0].clientY); |
| const text2 = document.getElementById("massEditText2").value; | | }, |
| const editType = document.getElementById("massEditType").value;
| | { passive: false } |
| const summary = document.getElementById("massEditSummary").value;
| | ); |
| const minor = document.getElementById("massEditMinor").checked;
| | document.addEventListener("touchend", onDragEnd); |
| | | document.addEventListener("touchcancel", onDragEnd); |
| await doMassEdit(pages, text1, text2, editType, summary, minor);
| |
| };
| |
| | |
| // Drag logic
| |
| let isDragging = false,
| |
| offsetX = 0,
| |
| offsetY = 0; | |
| | |
| function startDrag(x, y) {
| |
| isDragging = true;
| |
| const rect = menu.getBoundingClientRect(); | |
| offsetX = x - rect.left; | |
| offsetY = y - rect.top;
| |
| menu.style.right = "auto";
| |
| } | | } |
|
| |
|
| function doDrag(x, y) { | | function toggleMenu() { |
| if (isDragging) { | | menu.style.display = menu.style.display === "none" ? "block" : "none"; |
| menu.style.left = x - offsetX + "px";
| |
| menu.style.top = y - offsetY + "px";
| |
| }
| |
| } | | } |
|
| |
|
| function stopDrag() { | | makeDraggable(toggle, [], toggleMenu); |
| isDragging = false;
| | makeDraggable(menu, [textarea, runBtn]); |
| } | |
|
| |
|
| header.addEventListener("mousedown", function (e) { | | menu.appendChild(title); |
| startDrag(e.clientX, e.clientY);
| | menu.appendChild(textarea); |
| e.preventDefault();
| | menu.appendChild(runBtn); |
| }); | | document.body.appendChild(toggle); |
| | | document.body.appendChild(menu); |
| document.addEventListener("mousemove", function (e) {
| |
| doDrag(e.clientX, e.clientY);
| |
| });
| |
| | |
| document.addEventListener("mouseup", stopDrag); | |
| | |
| header.addEventListener("touchstart", function (e) {
| |
| const touch = e.touches[0];
| |
| startDrag(touch.clientX, touch.clientY);
| |
| e.preventDefault();
| |
| });
| |
| | |
| document.addEventListener("touchmove", function (e) { | |
| if (!isDragging) return;
| |
| const touch = e.touches[0];
| |
| doDrag(touch.clientX, touch.clientY);
| |
| });
| |
| | |
| document.addEventListener("touchend", stopDrag);
| |
| })(); | | })(); |