|
// ==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 [] |
|
} |
|
} |
@Korb I do not have much time to update the script and its description right now. But I will in the near future! Thanks for pointing to the problems.