|
// ==UserScript== |
|
// @name 2GIS phone number copier |
|
// @namespace http://2gis.ru/ |
|
// @license MIT |
|
// @version 0.5.0 |
|
// @description 2GIS mobile phone number copier allow you to copy any Russian mobile phone number to your device"s clipboard |
|
// @author Kenya-West |
|
// @include https://2gis.ru/* |
|
// @grant GM_setClipboard |
|
// @grant GM_getValue |
|
// @grant GM_setValue |
|
// ==/UserScript== |
|
|
|
// store url on load |
|
let currentPage = location.href; |
|
let recordList = getFromGM("recordList"); |
|
let tableMode = recordList.length ? true : false; |
|
urlCheckCounter = 0; |
|
|
|
// listen for changes |
|
setInterval(function() |
|
{ |
|
if (currentPage !== location.href || urlCheckCounter === 0) |
|
{ |
|
// page has changed, set new page as "current" |
|
currentPage = location.href; |
|
|
|
main(); |
|
} |
|
urlCheckCounter++; |
|
}, 175); |
|
|
|
function main() { |
|
|
|
// Grand-parent for parentElems |
|
const phoneContainer = document.querySelector("._172gbf8:has(>._49kxlr>._b0ke8)"); |
|
// Parents for hrefs |
|
const phoneContainerChild = document.querySelector("._172gbf8 ._49kxlr:has(>._b0ke8)"); |
|
|
|
// |
|
// INIT FUNCTION SET |
|
// |
|
|
|
if (expandTels()) { |
|
placeTableMode(); |
|
addListenersOnRender(); |
|
setTimeout(() => { |
|
placeIcons(); |
|
}, 100); |
|
// placeIcons(elemsMedia, parentElemsMedia, parentHrefsMedia, numberLinkMedia); |
|
} |
|
|
|
/** |
|
* Expands "Показать телефоны" button |
|
* |
|
* The top-level checking function |
|
* |
|
* @returns `boolean` |
|
*/ |
|
function expandTels() { |
|
const event = new Event("click", {bubbles: true}) |
|
|
|
const expandElem = document.querySelector("._1ns0i7c"); |
|
|
|
if (expandElem) { |
|
expandElem.dispatchEvent(event); |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
// |
|
// PLACE FUNCTION SET |
|
// |
|
|
|
function placeIcons() { |
|
// Parents for hrefs |
|
const parents = document.querySelectorAll("._49kxlr > ._b0ke8"); |
|
// Parent elements a[href] that are parents for elems |
|
const hrefs = document.querySelectorAll("._49kxlr > ._b0ke8 > a"); |
|
// Number link like `tel:` containing number. Usually it"s the hrefs[index] |
|
const telLink = document.querySelectorAll("._49kxlr ._b0ke8 > a"); |
|
// Elements that contain phone number in innerText |
|
const elems = document.querySelectorAll("._49kxlr > ._b0ke8 > a"); |
|
|
|
elems.forEach((element, index) => { |
|
|
|
let whatsappLink = preparePhoneNumber(telLink[index].href, index); |
|
let phoneLink = preparePhoneNumber(telLink[index].href, index); |
|
let phoneNumber = preparePhoneNumber(telLink[index].href, index); |
|
|
|
if (whatsappLink && !parents[index].getAttribute("whatsapp-linked")) { |
|
let whatsapp = "https://wa.me/" + whatsappLink; |
|
let button = prepareWhatsappButton(whatsapp, index); |
|
parents[index].insertBefore(button, hrefs[index]); |
|
parents[index].setAttribute("whatsapp-linked", true); |
|
storePhoneOrLink("whatsappLink", whatsapp); |
|
} |
|
if (phoneLink) { |
|
let phone = "tel:+" + phoneLink; |
|
} |
|
if (phoneNumber && !parents[index].getAttribute("phone-linked")) { |
|
let phone = `+${phoneNumber[0]} ${phoneNumber[1]}${phoneNumber[2]}${phoneNumber[3]} ${phoneNumber[4]}${phoneNumber[5]}${phoneNumber[6]}-${phoneNumber[7]}${phoneNumber[8]}-${phoneNumber[9]}${phoneNumber[10]}` |
|
let button = preparePhoneButton(phone, index); |
|
parents[index].insertBefore(button, hrefs[index]); |
|
parents[index].setAttribute("phone-linked", true); |
|
storePhoneOrLink("phone", phone); |
|
} |
|
|
|
updateState(); |
|
}); |
|
|
|
} |
|
|
|
function placeCounter() { |
|
let counter = prepareCounter(); |
|
|
|
if(counter) { |
|
// let element = prepareCounter(); |
|
phoneContainerChild.appendChild(counter); |
|
} |
|
|
|
updateState(); |
|
} |
|
|
|
function placeTableMode() { |
|
let tableModeButton = prepareTableModeButton(); |
|
phoneContainerChild.appendChild(tableModeButton); |
|
|
|
tableModeCheckbox.checked = tableMode; |
|
} |
|
|
|
function tableAddRemoveButtonManager(value) { |
|
if (value) { |
|
placeTableAddRemoveButton(); |
|
} else { |
|
removeTableAddRemoveButton(); |
|
} |
|
} |
|
function placeTableAddRemoveButton() { |
|
const host = phoneContainerChild.querySelector("#tableModeAddRemoveButtonHost"); |
|
|
|
if (!host) { |
|
let tableModeAddRemoveButton = prepareTableModeAddRemoveButton(); |
|
phoneContainerChild.appendChild(tableModeAddRemoveButton); |
|
|
|
phoneContainer.querySelector("#tableModeAddRemoveButton").addEventListener("click", (element) => { |
|
pushOrRemoveRecord(); |
|
}) |
|
} |
|
} |
|
function removeTableAddRemoveButton() { |
|
const elem = phoneContainerChild.querySelector("#tableModeAddRemoveButtonHost"); |
|
|
|
if (elem) { |
|
elem.parentNode.removeChild(elem) |
|
} |
|
} |
|
|
|
function tableCopyButtonManager(value) { |
|
if (value) { |
|
placeTableCopyButton(); |
|
} else { |
|
removeTableCopyButton(); |
|
} |
|
} |
|
function placeTableCopyButton() { |
|
const host = phoneContainerChild.querySelector("#tableCopyButtonHost"); |
|
|
|
if (!host) { |
|
let tableCopyButton = prepareTableCopyButton(); |
|
phoneContainerChild.appendChild(tableCopyButton); |
|
|
|
phoneContainer.querySelector("#tableCopyButton").addEventListener("click", (element) => { |
|
copyTable(); |
|
}) |
|
} |
|
} |
|
function removeTableCopyButton() { |
|
const elem = phoneContainerChild.querySelector("#tableCopyButtonHost"); |
|
|
|
if (elem) { |
|
elem.parentNode.removeChild(elem) |
|
} |
|
} |
|
|
|
function placeTable() { |
|
const data = recordList; |
|
|
|
// { |
|
// id: firmId ?? recordList.length, |
|
// name: firmName, |
|
// shortUrl: value.shorturl ?? "", |
|
// fullUrl: location.href, |
|
// phones: [...getPhonesOrLinks("phone")], |
|
// whatsappLinks: [...getPhonesOrLinks("whatsappLink")] |
|
// } |
|
|
|
phoneContainer.insertAdjacentHTML( |
|
`beforeend`, |
|
` |
|
<table id="contactsTable" bordercolor="gray" border="2"> |
|
<thead> |
|
<tr> |
|
<th>📄 Название</th> |
|
<th>📞 Номер</th> |
|
<th>🔗 Ссылка</th> |
|
<th>💭 Комментарий</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
${[...data] |
|
.map( |
|
(_, i) => `<tr>${[...Array(4)] |
|
.map( |
|
(_, j) => |
|
{ |
|
if (j===1) { return `<td><a href="${data[i].shortUrl ?? data[i].fullUrl}">${data[i].name}</a></td>`} |
|
if (j===2) { return `<td>${data[i].phones.map((_, k) => { |
|
return `${_}<br>` |
|
}).join("")}</td>`} |
|
if (j===3) { return `<td>${data[i].whatsappLinks.map((_, k) => { |
|
return `<a href="${_}">${_}</a><br>` |
|
}).join("")}</td>`} |
|
if (j===4) { return `<td></td>`} |
|
} |
|
) |
|
.join("")} |
|
</tr>` |
|
) |
|
.join("")} |
|
</tbody> |
|
</table> |
|
` |
|
); |
|
} |
|
function removeTable() { |
|
const elem = phoneContainer.querySelector("#contactsTable"); |
|
elem.parentNode.removeChild(elem); |
|
} |
|
|
|
// |
|
// PREPARE FUNCTION SET |
|
// |
|
|
|
function preparePhoneNumber(phone, index) { |
|
const regex = /79\d+/; |
|
if (regex.test(phone)) { |
|
let result = regex.exec(phone); |
|
if (result[0].length < 11) { |
|
return null; |
|
} |
|
return result[0] |
|
} else { return null } |
|
} |
|
|
|
function prepareWhatsappButton(whatsappLink, index) { |
|
const button = document.createElement("img"); |
|
button.id = "whatsappLink" + index; |
|
button.src = "https://upload.wikimedia.org/wikipedia/commons/1/19/WhatsApp_logo-color-vertical.svg"; |
|
button.style.width = "18px"; |
|
button.style.height = "18px"; |
|
button.style.display = "inline-block"; |
|
button.style.cursor = "pointer"; |
|
button.style.marginRight = "5px"; |
|
button.setAttribute("whatsapp-link", whatsappLink); |
|
button.addEventListener("click", (element) => { |
|
if (element.target && element.target.id === button.id) { |
|
GM_setClipboard(whatsappLink); |
|
element.toElement.style.backgroundImage = "green"; |
|
window.setTimeout(() => { |
|
element.toElement.style.backgroundImage = "none"; |
|
}, 1000); |
|
} |
|
}); |
|
return button; |
|
} |
|
function preparePhoneButton(phone, index) { |
|
const button = document.createElement("img"); |
|
button.id = "phone" + index; |
|
button.src = "https://upload.wikimedia.org/wikipedia/commons/8/83/Circle-icons-phone.svg"; |
|
button.style.width = "18px"; |
|
button.style.height = "18px"; |
|
button.style.display = "inline-block"; |
|
button.style.cursor = "pointer"; |
|
button.style.marginRight = "5px"; |
|
button.setAttribute("phone", phone); |
|
button.style.tra |
|
button.addEventListener("click", (element) => { |
|
if (element.target && element.target.id === button.id) { |
|
GM_setClipboard(phone); |
|
element.toElement.style.backgroundImage = "green"; |
|
window.setTimeout(() => { |
|
element.toElement.style.backgroundImage = "none"; |
|
}, 1000); |
|
} |
|
}); |
|
return button; |
|
} |
|
|
|
function prepareTableModeButton() { |
|
let innerHTML = `<ul> |
|
<li class="_gyromm" id="tableMode"> |
|
<label class="_vvxysz1" title="Режим таблицы" |
|
><div class="_okzfjf"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="black"> |
|
<path d="M19 18V6H5v15a5 5 0 0 0 5 5h12a5 5 0 0 0 5-5v-3zm-6 3a3 3 0 0 1-6 0V8h10v10h-4zm12 0a3 3 0 0 1-3 3h-8a5 5 0 0 0 1-3v-1h10z"></path> |
|
<path d="M9 10h6v2H9zM9 14h4v2H9z"></path> |
|
</svg> |
|
</div> |
|
<input id="tableModeCheckbox" class="_1u9fru1" type="checkbox" value=0 /><span |
|
class="_1iurgbx" |
|
>Режим таблицы</span |
|
></label |
|
> |
|
</li> |
|
</ul> |
|
` |
|
const button = document.createElement("div"); |
|
button.classList.add("_b0ke8"); |
|
button.innerHTML = innerHTML; |
|
return button; |
|
} |
|
|
|
function prepareTableModeAddRemoveButton() { |
|
let innerHTML = `<div class="_rtsy3" id="tableModeAddRemoveButtonHost" > |
|
<div class="_jro6t0"> |
|
<div class="_11pijuk"> |
|
<button id="tableModeAddRemoveButton" class="_1uwefnfu" type="button">Добавить</button> |
|
</div> |
|
</div> |
|
</div> |
|
` |
|
const button = document.createElement("div"); |
|
button.innerHTML = innerHTML; |
|
return button; |
|
} |
|
|
|
function prepareTableCopyButton() { |
|
let innerHTML = `<div class="_rtsy3" id="tableCopyButtonHost" > |
|
<div class="_jro6t0"> |
|
<div class="_11pijuk"> |
|
<button id="tableCopyButton" class="_1uwefnfu" style="background-color: #f2f2f2;color: black;margin-top: 5px;" type="button">Копировать</button> |
|
</div> |
|
</div> |
|
</div> |
|
` |
|
const button = document.createElement("div"); |
|
button.innerHTML = innerHTML; |
|
return button; |
|
} |
|
|
|
function prepareCounter() { |
|
const counter = document.createElement("div"); |
|
counter.classList.add("_b0ke8"); |
|
counter.setAttribute("id", "recordListCounter"); |
|
return counter; |
|
} |
|
|
|
// |
|
// LISTENERS AND SERVICES |
|
// |
|
|
|
function updateState() { |
|
updateCounter(); |
|
changeTableModeAddRemoveButtonState(phoneContainerChild.querySelector("#tableModeAddRemoveButton")); |
|
storeValue("recordList", recordList); |
|
tableModeValueChangeHandler(tableMode); |
|
tableAddRemoveButtonManager(tableMode); |
|
tableCopyButtonManager(recordList.length > 0); |
|
} |
|
|
|
function updateCounter() { |
|
const counter = phoneContainer.querySelector("#recordListCounter"); |
|
if (counter) { |
|
counter.innerText = `Количество материалов: ${recordList.length}`; |
|
|
|
if (!tableMode) { |
|
counter.parentNode.removeChild(counter) |
|
} |
|
} else if (!counter && tableMode) { |
|
placeCounter(); |
|
} |
|
|
|
} |
|
|
|
function changeTableModeAddRemoveButtonState(elem) { |
|
const firmId = /firm\/(\d+)/gi.exec(location.href)[1]; |
|
if (phoneContainerChild.querySelector("#tableModeAddRemoveButtonHost")) { |
|
if (recordList.find((record) => record.id === firmId)) { |
|
elem.textContent = "Удалить"; |
|
elem.style.backgroundColor = "#299400"; |
|
elem.style.color = "white"; |
|
} else { |
|
elem.textContent = "Добавить"; |
|
elem.style.backgroundColor = "#f2f2f2"; |
|
elem.style.color = "black"; |
|
} |
|
} |
|
} |
|
|
|
function addListenersOnRender() { |
|
const tableModeHost = phoneContainer.querySelector("#tableMode"); |
|
const tableModeCheckbox = phoneContainer.querySelector("#tableModeCheckbox"); |
|
|
|
if (tableModeHost) { |
|
tableModeHost.addEventListener("click", (element) => { |
|
tableModeCheckbox.checked = !tableModeCheckbox.checked; |
|
tableMode = tableModeCheckbox.checked; |
|
updateState(); |
|
}); |
|
} |
|
} |
|
|
|
function tableModeValueChangeHandler(checked) { |
|
// change view |
|
const host = phoneContainer.querySelector("#tableMode"); |
|
const label = host?.querySelector("._vvxysz1, ._1t53cmw5"); |
|
const icon = host?.querySelector("._okzfjf, ._1laf6tw8"); |
|
|
|
if (host && label && icon) { |
|
if (checked) { |
|
label.style.backgroundColor = "rgb(48, 173, 0)"; |
|
label.style.color = "white"; |
|
icon.querySelector("svg").setAttribute("fill", "white"); |
|
} else { |
|
label.style.backgroundColor = "inherit"; |
|
label.style.color = "initial"; |
|
icon.querySelector("svg").setAttribute("fill", "back"); |
|
} |
|
} |
|
} |
|
|
|
function storeValue(key, value) { |
|
GM_setValue(JSON.stringify(key, value)); |
|
} |
|
|
|
// DATA PARSING |
|
const _phones = []; |
|
const _whatsappLinks = []; |
|
/** |
|
* Sets phones or links to a runtime var |
|
* @param {string} mode "phone", "whatsappLink" |
|
* @param {string} data string value (phone or link) |
|
*/ |
|
function storePhoneOrLink(mode, data) { |
|
switch (mode) { |
|
case "phone": |
|
_phones.push(data) |
|
break; |
|
case "whatsappLink": |
|
_whatsappLinks.push(data) |
|
break; |
|
} |
|
} |
|
/** |
|
* Returns phones or links from a runtime var |
|
* @param {string} mode "phone", "whatsappLink" |
|
*/ |
|
function getPhonesOrLinks(mode) { |
|
switch (mode) { |
|
case "phone": |
|
return _phones; |
|
case "whatsappLink": |
|
return _whatsappLinks; |
|
} |
|
} |
|
|
|
async function fetchData() { |
|
let response = await fetch(`https://go.2gis.com/add/?mode=json&encoded=true&url=${encodeURI(location.href)}`); |
|
return response.json(); |
|
} |
|
|
|
function pushOrRemoveRecord() { |
|
const firmId = /firm\/(\d+)/gi.exec(location.href)[1]; |
|
const firmName = document.querySelector("._6htn2u ._oqoid").innerText; |
|
|
|
// removes a record |
|
if (recordList.find((record) => record.id === firmId)) { |
|
recordList = recordList.filter((record) => record.id !== firmId); |
|
} |
|
// adds a record |
|
else { |
|
fetchData().then((value) => { |
|
recordList.push({ |
|
id: firmId ?? recordList.length, |
|
name: firmName, |
|
shortUrl: value.shorturl ?? "", |
|
fullUrl: location.href, |
|
phones: [...getPhonesOrLinks("phone")], |
|
whatsappLinks: [...getPhonesOrLinks("whatsappLink")] |
|
}); |
|
}) |
|
.finally(() => updateState()) |
|
} |
|
|
|
updateState(); |
|
} |
|
|
|
function copyTable() { |
|
placeTable(); |
|
const table = phoneContainer.querySelector("#contactsTable"); |
|
|
|
try { |
|
// create a Range object |
|
var range = document.createRange(); |
|
var sel = window.getSelection(); |
|
// set the Node to select the "range" |
|
range.selectNode(table); |
|
sel.removeAllRanges(); |
|
sel.addRange(range); |
|
// add the Range to the set of window selections |
|
// window.getSelection().addRange(range); |
|
|
|
// execute 'copy', can't 'cut' in this case |
|
document.execCommand('copy'); |
|
} |
|
catch(e) {} |
|
|
|
|
|
removeTable(); |
|
} |
|
} |
|
|
|
// |
|
// FUNCTIONS THAT NEED GLOBAL SCOPE |
|
// |
|
|
|
function getFromGM(key) { |
|
try { |
|
return JSON.parse(GM_getValue(key)); |
|
} catch (error) { |
|
return [] |
|
} |
|
} |
This script copies phone number in Whatsapp format. It places copy icon before phone number. When you click on the icon, it copies phone as
https://api.whatsapp.com/send?phone=<PhoneNumber>
.For example, the phone number on your screenshot would be copied as
https://api.whatsapp.com/send?phone=78129803808
.Why so ugly link? Well, several years ago the only format Whatsapp understands to follow an account associated with phone number was this exact link scheme.
Now, Whatsapp redirects to whatsapp account just by bare phone number text, and thus script is mostly not irrelevant. And I really dunno whether it works or not.