// ==UserScript==
// @name PropertyguruAssist
// @namespace http://tampermonkey.net/
// @version 0.99
// @description one button click -> simplify propertyguru listing info for easily copy / paste
// @author EnginePlus
// @match https://*.propertyguru.com.sg/listing/*
// @match https://*.commercialguru.com.sg/listing/*
// @match https://*.propertyguru.com.my/property-listing/*
// @grant none
// @resource customCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
// @require https://greasyfork.org/scripts/27254-clipboard-js/code/clipboardjs.js?version=174357
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.slim.min.js
// ==/UserScript==
(function () {
'use strict';
function getValueByLabel(items, label) {
if (!Array.isArray(items)) return 'N.A.';
let item = items.find(item => item.label === label || item.icon === label);
return item ? item.value : 'N.A.';
}
function safe(fn, fallback = 'N.A.') {
try {
const val = fn();
return (val !== undefined && val !== null && val !== '') ? val : fallback;
} catch {
return fallback;
}
}
function isLoginRequired() {
const buttons = Array.from(document.querySelectorAll('div.btn-content'));
return buttons.some(el => el.textContent.trim().toLowerCase() === 'login');
}
function extractData() {
const jsonData = safe(() => JSON.parse(document.getElementById('__NEXT_DATA__').textContent), {});
const root = jsonData?.props?.pageProps?.pageData?.data;
const metatableItems = safe(() => root.detailsData.metatable.items, []);
return {
url: window.location.href,
propertyName: safe(() => root.listingData.localizedTitle),
tenureType: getValueByLabel(metatableItems, 'calendar-days-o'),
topYear: getValueByLabel(metatableItems, 'document-with-lines-o'),
totalUnits: getValueByLabel(metatableItems, 'block-o'),
bedNum: safe(() => root.listingData.bedrooms),
bathNum: safe(() => root.listingData.bathrooms),
floorSize: safe(() => root.listingData.floorArea),
price: safe(() => root.propertyOverviewData.propertyInfo.price.amount),
agentName: safe(() => root.contactAgentData.contactAgentCard.agentInfoProps.agent.name),
phoneNumber: safe(() => root.listingData.agent.mobile || root.contactAgentData.contactAgentCard.contactActions?.[0]?.phoneNumber)
};
}
function copyToClipboard(data, checkboxes, button) {
const get = key => checkboxes[key]?.checked ? (data[key] || 'N.A.') : '';
const mainInfo = get('propertyName') + ' [' + get('tenureType') + ' / ' + get('topYear') + ' / ' + get('totalUnits') + ']'
+ ', ' + get('bedNum') + ' Bed, ' + get('bathNum') + ' Bath, ' + get('floorSize') + ' sqft, ' + get('price');
const agentInfo = get('agentName') + ' ' + get('phoneNumber');
const clipboardText = data.url + '\t' + mainInfo + '\t' + agentInfo;
navigator.clipboard.writeText(clipboardText).then(() => {
if (button) {
button.textContent = '已复制!';
setTimeout(() => (button.textContent = '复制到剪贴板'), 2000);
}
});
}
// ✅ 构造 WhatsApp 链接函数
function formatPhoneNumber(raw) {
const phone = raw.replace(/\D/g, '');
return phone.startsWith('65') ? phone : '65' + phone;
}
function buildWhatsAppLink(agentName, listingUrl, rawPhone) {
const phone = formatPhoneNumber(rawPhone);
const message = `Hi ${agentName}, My client would like to learn more about your listing: ${listingUrl}`;
const encodedText = encodeURIComponent(message);
return `https://api.whatsapp.com/send?phone=${phone}&text=${encodedText}`;
}
function createPanel(data) {
const groups = [
{ key: 'infoBlock', label: '房源基本信息', fields: ['propertyName', 'tenureType', 'topYear', 'totalUnits', 'bedNum', 'bathNum', 'floorSize', 'price'] },
{ key: 'agentBlock', label: '联系人信息', fields: ['agentName', 'phoneNumber'] }
];
const panel = document.createElement('div');
panel.style.position = 'fixed';
panel.style.bottom = '20px';
panel.style.right = '20px';
panel.style.background = '#fdfdfd';
panel.style.border = '1px solid #ccc';
panel.style.borderRadius = '8px';
panel.style.boxShadow = '2px 2px 12px rgba(0,0,0,0.2)';
panel.style.fontSize = '14px';
panel.style.minWidth = '320px';
panel.style.zIndex = 9999;
panel.style.cursor = 'move';
panel.setAttribute("id", "floatingPanel");
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.padding = '8px';
header.style.backgroundColor = '#4A90E2';
header.style.color = '#fff';
header.style.borderTopLeftRadius = '8px';
header.style.borderTopRightRadius = '8px';
header.style.cursor = 'move';
const titleText = document.createElement('span');
titleText.textContent = 'v0.99 小助手提取信息';
titleText.style.fontWeight = 'bold';
header.appendChild(titleText);
if (isLoginRequired()) {
const warn = document.createElement('span');
warn.textContent = '未登录 可能不显示联络号码';
warn.style.color = 'red';
warn.style.fontWeight = 'normal';
warn.style.fontSize = '12px';
warn.style.marginLeft = '10px';
header.appendChild(warn);
}
panel.appendChild(header);
const content = document.createElement('div');
content.style.padding = '10px';
content.style.backgroundColor = '#ffffff';
const checkboxes = {};
groups.forEach(group => {
const title = document.createElement('div');
title.textContent = group.label;
title.style.fontWeight = 'bold';
title.style.marginTop = '10px';
content.appendChild(title);
group.fields.forEach(key => {
const wrapper = document.createElement('div');
wrapper.style.marginBottom = '4px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
checkbox.id = key;
checkboxes[key] = checkbox;
const label = document.createElement('label');
label.htmlFor = key;
label.textContent = ' ' + key + ': ';
const valueSpan = document.createElement('span');
valueSpan.style.color = '#333';
valueSpan.textContent = data[key] || 'N.A.';
wrapper.appendChild(checkbox);
wrapper.appendChild(label);
wrapper.appendChild(valueSpan);
content.appendChild(wrapper);
});
});
const button = document.createElement('button');
button.textContent = '复制到剪贴板';
button.style.padding = '5px 10px';
button.style.fontSize = '13px';
button.style.cursor = 'pointer';
button.onclick = () => copyToClipboard(data, checkboxes, button);
const buttonGroup = document.createElement('div');
buttonGroup.style.marginTop = '10px';
buttonGroup.appendChild(button);
// ✅ 增加带文本的 WhatsApp 联系按钮
if (data.phoneNumber && data.phoneNumber !== 'N.A.') {
const whatsappBtn = document.createElement('button');
whatsappBtn.textContent = 'WhatsApp联系中介';
whatsappBtn.style.marginLeft = '10px';
whatsappBtn.style.padding = '5px 10px';
whatsappBtn.style.fontSize = '13px';
whatsappBtn.style.cursor = 'pointer';
whatsappBtn.onclick = () => {
const url = buildWhatsAppLink(data.agentName, data.url, data.phoneNumber);
window.open(url, '_blank');
};
buttonGroup.appendChild(whatsappBtn);
}
content.appendChild(buttonGroup);
panel.appendChild(content);
document.body.appendChild(panel);
makeDraggable(panel, header);
copyToClipboard(data, checkboxes, button);
}
function makeDraggable(panel, handle) {
let isDragging = false;
let offsetX, offsetY;
handle.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - panel.getBoundingClientRect().left;
offsetY = e.clientY - panel.getBoundingClientRect().top;
panel.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
panel.style.left = `${e.clientX - offsetX}px`;
panel.style.top = `${e.clientY - offsetY}px`;
panel.style.bottom = 'auto';
panel.style.right = 'auto';
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
panel.style.cursor = 'move';
});
}
if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", () => {
const data = extractData();
createPanel(data);
});
} else {
const data = extractData();
createPanel(data);
}
})();