From e8cbce3ca5bd3fe9c7ac9c21fbc56ae60f8fd408 Mon Sep 17 00:00:00 2001 From: Rishab Dugar Date: Mon, 5 Oct 2026 02:51:41 +0000 Subject: [PATCH] fix(redact): let a finger scroll the pages on touch screens On phones the page fills the screen, and a finger on it always drew a mark, so there was no way to reach page 2. A finger or pen now scrolls, and a "Draw boxes" switch (shown only on touch screens) turns drawing on. A mouse still draws as before. Part of #85. --- src/i18n.ts | 2 ++ src/i18n/bn.json | 2 ++ src/i18n/de.json | 2 ++ src/i18n/es.json | 2 ++ src/i18n/fr.json | 2 ++ src/i18n/hi.json | 2 ++ src/i18n/id.json | 2 ++ src/i18n/it.json | 2 ++ src/i18n/mr.json | 2 ++ src/i18n/nl.json | 2 ++ src/i18n/pl.json | 2 ++ src/i18n/pt.json | 2 ++ src/i18n/ta.json | 2 ++ src/i18n/te.json | 2 ++ src/i18n/tr.json | 2 ++ src/i18n/vi.json | 2 ++ src/tools/redact-pdf.css | 28 +++++++++++++++++++++++++++- src/tools/redact-pdf.ts | 21 +++++++++++++++++---- tests/app.e2e.ts | 36 ++++++++++++++++++++++++++++++++++++ 19 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 87f6240..76e598c 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -288,6 +288,8 @@ export const UI = { 'rd.personalFound': 'Personal details marked: {n}. Check each one before saving.', 'rd.personalNone': 'No emails, phone, ID or card numbers found. Search for names and addresses above.', 'rd.remove': 'Remove this mark', + 'rd.draw': 'Draw boxes', + 'rd.touchHint': 'On a touch screen, swipe to scroll. Tap “Draw boxes” to mark with your finger, and tap it again to scroll.', 'rd.hint': 'Drag on a page to cover anything: text, a signature, a photo. When you save, every page becomes an image, so what you covered is gone for good.', 'ocr.unsure': 'Some of this text may be wrong. Text recognition reads English that is upright: rotate sideways photos first, and expect other scripts to come out garbled.', 'ocr.copyAll': 'Copy all text', diff --git a/src/i18n/bn.json b/src/i18n/bn.json index 2eb4ef2..b84144d 100644 --- a/src/i18n/bn.json +++ b/src/i18n/bn.json @@ -231,6 +231,8 @@ "rd.personalFound": "ব্যক্তিগত তথ্য চিহ্নিত: {n}। সেভ করার আগে প্রতিটি দেখে নিন।", "rd.personalNone": "কোনো ইমেল, ফোন, ID বা কার্ড নম্বর পাওয়া যায়নি। নাম ও ঠিকানা ওপরে খুঁজুন।", "rd.remove": "এই মার্কটা সরান", + "rd.draw": "বক্স আঁকুন", + "rd.touchHint": "টাচ স্ক্রিনে স্ক্রল করতে সোয়াইপ করুন। আঙুল দিয়ে চিহ্ন দিতে “বক্স আঁকুন”-এ ট্যাপ করুন, আর স্ক্রল করতে আবার ট্যাপ করুন।", "rd.hint": "যেকোনো কিছু ঢাকতে পাতায় ড্র্যাগ করুন: টেক্সট, স্বাক্ষর, ছবি। সেভ করলে প্রতিটা পাতা একটা ছবিতে পরিণত হয়, তাই যা ঢেকেছেন তা চিরতরে মুছে যায়।", "footer.madeBy": "ডিজাইন ও নির্মাণ:", "footer.feedback": "বাগ রিপোর্ট করুন বা টুল প্রস্তাব করুন", diff --git a/src/i18n/de.json b/src/i18n/de.json index f768891..51dd9e4 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -231,6 +231,8 @@ "rd.personalFound": "Persönliche Daten markiert: {n}. Prüfen Sie jede Markierung vor dem Speichern.", "rd.personalNone": "Keine E-Mails, Telefon-, Ausweis- oder Kartennummern gefunden. Suchen Sie Namen und Adressen oben.", "rd.remove": "Diese Markierung entfernen", + "rd.draw": "Rahmen zeichnen", + "rd.touchHint": "Auf einem Touchscreen wischen Sie zum Scrollen. Tippen Sie auf „Rahmen zeichnen“, um mit dem Finger zu markieren, und erneut, um zu scrollen.", "rd.hint": "Ziehen Sie über eine Seite, um etwas zu bedecken: Text, eine Unterschrift, ein Foto. Beim Speichern wird jede Seite zu einem Bild, sodass das Bedeckte für immer verschwindet.", "footer.madeBy": "Entworfen und entwickelt von", "footer.feedback": "Fehler melden oder Tool vorschlagen", diff --git a/src/i18n/es.json b/src/i18n/es.json index dd130e9..c36cd64 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -231,6 +231,8 @@ "rd.personalFound": "Datos personales marcados: {n}. Revisa cada uno antes de guardar.", "rd.personalNone": "No se encontraron correos, teléfonos ni números de ID o tarjeta. Busca nombres y direcciones arriba.", "rd.remove": "Quitar esta marca", + "rd.draw": "Dibujar cuadros", + "rd.touchHint": "En una pantalla táctil, desliza para desplazarte. Toca «Dibujar cuadros» para marcar con el dedo y vuelve a tocarlo para desplazarte.", "rd.hint": "Arrastra sobre una página para tapar cualquier cosa: texto, una firma, una foto. Al guardar, cada página se convierte en una imagen, así que lo que tapaste desaparece para siempre.", "footer.madeBy": "Diseñado y creado por", "footer.feedback": "Reporta un error o sugiere una herramienta", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index e65da6b..1d1cf2d 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -231,6 +231,8 @@ "rd.personalFound": "Données personnelles marquées : {n}. Vérifiez chacune avant d’enregistrer.", "rd.personalNone": "Aucun e-mail, téléphone ni numéro d’identité ou de carte trouvé. Recherchez les noms et adresses ci-dessus.", "rd.remove": "Supprimer cette marque", + "rd.draw": "Dessiner des cadres", + "rd.touchHint": "Sur un écran tactile, balayez pour faire défiler. Touchez « Dessiner des cadres » pour marquer au doigt, puis touchez à nouveau pour faire défiler.", "rd.hint": "Faites glisser sur une page pour couvrir n'importe quoi : du texte, une signature, une photo. À l'enregistrement, chaque page devient une image, donc ce que vous avez couvert disparaît pour de bon.", "footer.madeBy": "Conçu et développé par", "footer.feedback": "Signaler un bug ou proposer un outil", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index d0886d1..3f8b1d1 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -231,6 +231,8 @@ "rd.personalFound": "निजी जानकारी मार्क की गई: {n}. सेव करने से पहले हर एक को जाँच लें।", "rd.personalNone": "कोई ईमेल, फ़ोन, ID या कार्ड नंबर नहीं मिला। नाम और पते ऊपर खोजें।", "rd.remove": "यह मार्क हटाएँ", + "rd.draw": "बॉक्स बनाएं", + "rd.touchHint": "टच स्क्रीन पर स्क्रॉल करने के लिए स्वाइप करें। उंगली से निशान लगाने के लिए “बॉक्स बनाएं” पर टैप करें, और स्क्रॉल करने के लिए फिर से टैप करें।", "rd.hint": "किसी भी चीज़ को कवर करने के लिए पेज पर ड्रैग करें: टेक्स्ट, हस्ताक्षर, फ़ोटो। सेव करते ही हर पेज एक इमेज बन जाता है, इसलिए जो कवर किया वह हमेशा के लिए मिट जाता है।", "footer.madeBy": "डिज़ाइन और निर्माण:", "footer.feedback": "बग रिपोर्ट करें या टूल सुझाएँ", diff --git a/src/i18n/id.json b/src/i18n/id.json index a9a0e4b..b00d7d9 100644 --- a/src/i18n/id.json +++ b/src/i18n/id.json @@ -231,6 +231,8 @@ "rd.personalFound": "Data pribadi ditandai: {n}. Periksa satu per satu sebelum menyimpan.", "rd.personalNone": "Tidak ada email, telepon, nomor identitas, atau kartu. Cari nama dan alamat di atas.", "rd.remove": "Hapus tanda ini", + "rd.draw": "Gambar kotak", + "rd.touchHint": "Di layar sentuh, geser untuk menggulir. Ketuk “Gambar kotak” untuk menandai dengan jari, lalu ketuk lagi untuk menggulir.", "rd.hint": "Seret pada halaman untuk menutup apa saja: teks, tanda tangan, foto. Saat disimpan, setiap halaman menjadi gambar, jadi yang Anda tutup akan hilang selamanya.", "footer.madeBy": "Dirancang dan dibuat oleh", "footer.feedback": "Laporkan bug atau usulkan alat", diff --git a/src/i18n/it.json b/src/i18n/it.json index b582d41..388d5ed 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -231,6 +231,8 @@ "rd.personalFound": "Dati personali segnati: {n}. Controllali uno per uno prima di salvare.", "rd.personalNone": "Nessuna email, telefono o numero di documento o carta trovato. Cerca nomi e indirizzi qui sopra.", "rd.remove": "Rimuovi questo segno", + "rd.draw": "Disegna riquadri", + "rd.touchHint": "Su uno schermo touch, scorri con il dito. Tocca «Disegna riquadri» per segnare con il dito e toccalo di nuovo per scorrere.", "rd.hint": "Trascina su una pagina per coprire qualsiasi cosa: testo, una firma, una foto. Al salvataggio, ogni pagina diventa un'immagine, quindi ciò che hai coperto sparisce per sempre.", "footer.madeBy": "Progettato e sviluppato da", "footer.feedback": "Segnala un bug o suggerisci uno strumento", diff --git a/src/i18n/mr.json b/src/i18n/mr.json index 7d8422e..7c4ef44 100644 --- a/src/i18n/mr.json +++ b/src/i18n/mr.json @@ -231,6 +231,8 @@ "rd.personalFound": "वैयक्तिक माहिती मार्क केली: {n}. सेव्ह करण्यापूर्वी प्रत्येक तपासा.", "rd.personalNone": "कोणताही ईमेल, फोन, ID किंवा कार्ड नंबर सापडला नाही. नावे आणि पत्ते वर शोधा.", "rd.remove": "हा मार्क काढा", + "rd.draw": "बॉक्स काढा", + "rd.touchHint": "टच स्क्रीनवर स्क्रोल करण्यासाठी स्वाइप करा. बोटाने खूण करण्यासाठी “बॉक्स काढा” वर टॅप करा, आणि स्क्रोल करण्यासाठी पुन्हा टॅप करा.", "rd.hint": "काहीही झाकण्यासाठी पानावर ड्रॅग करा: मजकूर, स्वाक्षरी, फोटो. सेव्ह केल्यावर प्रत्येक पान इमेज बनते, त्यामुळे तुम्ही झाकलेले कायमचे नाहीसे होते.", "footer.madeBy": "डिझाइन आणि निर्मिती:", "footer.feedback": "बग कळवा किंवा टूल सुचवा", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 2a6a8c4..0bef341 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -231,6 +231,8 @@ "rd.personalFound": "Persoonsgegevens gemarkeerd: {n}. Controleer ze allemaal voor het opslaan.", "rd.personalNone": "Geen e-mails, telefoon-, ID- of kaartnummers gevonden. Zoek namen en adressen hierboven.", "rd.remove": "Deze markering verwijderen", + "rd.draw": "Vakken tekenen", + "rd.touchHint": "Veeg op een touchscreen om te scrollen. Tik op “Vakken tekenen” om met je vinger te markeren, en tik nog eens om te scrollen.", "rd.hint": "Sleep over een pagina om iets te bedekken: tekst, een handtekening, een foto. Bij het opslaan wordt elke pagina een afbeelding, dus wat je bedekte is voorgoed weg.", "footer.madeBy": "Ontworpen en gebouwd door", "footer.feedback": "Meld een bug of stel een tool voor", diff --git a/src/i18n/pl.json b/src/i18n/pl.json index 21dcda3..fa731f9 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -231,6 +231,8 @@ "rd.personalFound": "Zaznaczone dane osobowe: {n}. Sprawdź każde przed zapisaniem.", "rd.personalNone": "Nie znaleziono e-maili, telefonów ani numerów dokumentów lub kart. Wyszukaj imiona i adresy powyżej.", "rd.remove": "Usuń ten znacznik", + "rd.draw": "Rysuj ramki", + "rd.touchHint": "Na ekranie dotykowym przesuń palcem, aby przewijać. Stuknij „Rysuj ramki”, aby zaznaczać palcem, i stuknij ponownie, aby przewijać.", "rd.hint": "Przeciągnij po stronie, aby zakryć cokolwiek: tekst, podpis, zdjęcie. Po zapisaniu każda strona staje się obrazem, więc to, co zakryjesz, znika na dobre.", "footer.madeBy": "Zaprojektował i zbudował", "footer.feedback": "Zgłoś błąd lub zaproponuj narzędzie", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 00c7d24..b0c1deb 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -231,6 +231,8 @@ "rd.personalFound": "Dados pessoais marcados: {n}. Confira cada um antes de salvar.", "rd.personalNone": "Nenhum e-mail, telefone ou número de documento ou cartão encontrado. Pesquise nomes e endereços acima.", "rd.remove": "Remover esta marca", + "rd.draw": "Desenhar caixas", + "rd.touchHint": "Em uma tela sensível ao toque, deslize para rolar. Toque em “Desenhar caixas” para marcar com o dedo e toque de novo para rolar.", "rd.hint": "Arraste sobre uma página para cobrir qualquer coisa: texto, uma assinatura, uma foto. Ao salvar, cada página vira uma imagem, então o que você cobriu some de vez.", "footer.madeBy": "Projetado e criado por", "footer.feedback": "Relate um erro ou sugira uma ferramenta", diff --git a/src/i18n/ta.json b/src/i18n/ta.json index 504e4fb..9e97121 100644 --- a/src/i18n/ta.json +++ b/src/i18n/ta.json @@ -231,6 +231,8 @@ "rd.personalFound": "குறிக்கப்பட்ட தனிப்பட்ட விவரங்கள்: {n}. சேமிக்கும் முன் ஒவ்வொன்றையும் சரிபார்க்கவும்.", "rd.personalNone": "மின்னஞ்சல், போன், ID அல்லது கார்டு எண் எதுவும் இல்லை. பெயர், முகவரிகளை மேலே தேடவும்.", "rd.remove": "இந்த மார்க்கை நீக்கு", + "rd.draw": "பெட்டிகளை வரை", + "rd.touchHint": "தொடுதிரையில் உருட்ட ஸ்வைப் செய்யவும். விரலால் குறிக்க “பெட்டிகளை வரை” என்பதைத் தட்டவும்; மீண்டும் உருட்ட மறுபடி தட்டவும்.", "rd.hint": "எதையும் மறைக்க பக்கத்தில் இழுக்கவும்: உரை, கையொப்பம், புகைப்படம். சேமிக்கும்போது ஒவ்வொரு பக்கமும் ஒரு படமாக மாறும், எனவே நீங்கள் மறைத்தது என்றென்றைக்கும் போய்விடும்.", "footer.madeBy": "வடிவமைத்து உருவாக்கியவர்", "footer.feedback": "பிழையைப் புகாரளிக்கவும் அல்லது கருவியைப் பரிந்துரைக்கவும்", diff --git a/src/i18n/te.json b/src/i18n/te.json index 64cba2d..00d6379 100644 --- a/src/i18n/te.json +++ b/src/i18n/te.json @@ -231,6 +231,8 @@ "rd.personalFound": "గుర్తించిన వ్యక్తిగత వివరాలు: {n}. సేవ్ చేసే ముందు ప్రతిదీ చెక్ చేయండి.", "rd.personalNone": "ఈమెయిల్, ఫోన్, ID లేదా కార్డ్ నంబర్ ఏదీ దొరకలేదు. పేర్లు, చిరునామాలను పైన వెతకండి.", "rd.remove": "ఈ మార్క్‌ను తీసివేయండి", + "rd.draw": "బాక్స్‌లు గీయండి", + "rd.touchHint": "టచ్ స్క్రీన్‌పై స్క్రోల్ చేయడానికి స్వైప్ చేయండి. వేలితో గుర్తు పెట్టడానికి “బాక్స్‌లు గీయండి” నొక్కండి, మళ్లీ స్క్రోల్ చేయడానికి మళ్లీ నొక్కండి.", "rd.hint": "దేన్నైనా కప్పడానికి పేజీపై డ్రాగ్ చేయండి: టెక్స్ట్, సంతకం, ఫోటో. సేవ్ చేసినప్పుడు ప్రతి పేజీ ఒక ఇమేజ్‌గా మారుతుంది, కాబట్టి మీరు కప్పింది శాశ్వతంగా పోతుంది.", "footer.madeBy": "డిజైన్ చేసి నిర్మించినది", "footer.feedback": "బగ్ నివేదించండి లేదా టూల్ సూచించండి", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 5d5a9e1..700c48f 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -231,6 +231,8 @@ "rd.personalFound": "İşaretlenen kişisel bilgi: {n}. Kaydetmeden önce her birini kontrol edin.", "rd.personalNone": "E-posta, telefon, kimlik veya kart numarası bulunamadı. Ad ve adresleri yukarıda arayın.", "rd.remove": "Bu işareti kaldır", + "rd.draw": "Kutu çiz", + "rd.touchHint": "Dokunmatik ekranda kaydırmak için parmağınızı sürükleyin. Parmakla işaretlemek için “Kutu çiz”e dokunun, kaydırmak için yeniden dokunun.", "rd.hint": "Bir şeyi örtmek için sayfada sürükleyin: metin, imza, fotoğraf. Kaydettiğinizde her sayfa bir görsele dönüşür, böylece örttüğünüz şey kalıcı olarak kaybolur.", "footer.madeBy": "Tasarlayan ve geliştiren", "footer.feedback": "Hata bildir veya araç öner", diff --git a/src/i18n/vi.json b/src/i18n/vi.json index 5713800..61c70c4 100644 --- a/src/i18n/vi.json +++ b/src/i18n/vi.json @@ -231,6 +231,8 @@ "rd.personalFound": "Đã đánh dấu thông tin cá nhân: {n}. Hãy kiểm tra từng mục trước khi lưu.", "rd.personalNone": "Không tìm thấy email, số điện thoại, số giấy tờ hay số thẻ. Hãy tìm tên và địa chỉ ở trên.", "rd.remove": "Xóa vết đánh dấu này", + "rd.draw": "Vẽ khung", + "rd.touchHint": "Trên màn hình cảm ứng, vuốt để cuộn. Chạm “Vẽ khung” để đánh dấu bằng ngón tay, chạm lại để cuộn.", "rd.hint": "Kéo trên một trang để che bất cứ thứ gì: văn bản, chữ ký, ảnh. Khi lưu, mỗi trang trở thành một hình ảnh, nên phần bạn che sẽ biến mất vĩnh viễn.", "footer.madeBy": "Thiết kế và phát triển bởi", "footer.feedback": "Báo lỗi hoặc đề xuất công cụ", diff --git a/src/tools/redact-pdf.css b/src/tools/redact-pdf.css index 719d5f1..18c763b 100644 --- a/src/tools/redact-pdf.css +++ b/src/tools/redact-pdf.css @@ -99,7 +99,33 @@ position: absolute; inset: 0; cursor: crosshair; - touch-action: none; /* a finger draws on the page; scroll in the grey space around it */ + touch-action: pan-x pan-y pinch-zoom; /* a finger scrolls until "Draw boxes" is on */ +} + +.redact-pdf.drawing .redact-overlay { + touch-action: none; +} + +.redact-btn[aria-pressed='true'] { + border-color: var(--accent); + background: var(--accent); + color: #fff; +} + +/* The draw switch and its hint only matter on touch screens. */ +.redact-draw, +.redact-touch-hint { + display: none; +} + +@media (any-pointer: coarse) { + .redact-draw { + display: inline-block; + } + + .redact-touch-hint { + display: block; + } } .redact-box { diff --git a/src/tools/redact-pdf.ts b/src/tools/redact-pdf.ts index d70050e..ca100f9 100644 --- a/src/tools/redact-pdf.ts +++ b/src/tools/redact-pdf.ts @@ -142,6 +142,14 @@ export async function openRedactor(file: File, viewer: HTMLElement): Promise { + const on = draw.getAttribute('aria-pressed') !== 'true'; + draw.setAttribute('aria-pressed', String(on)); + viewer.classList.toggle('drawing', on); + }; const undo = button(t('ed.undo')); const clear = button(t('rd.clear')); const count = document.createElement('span'); @@ -149,14 +157,17 @@ export async function openRedactor(file: File, viewer: HTMLElement): Promise { const viewport = view.viewport; if (!viewport || event.button !== 0) return; + if (event.pointerType !== 'mouse' && !viewer.classList.contains('drawing')) return; event.preventDefault(); view.overlay.setPointerCapture(event.pointerId); const bounds = view.overlay.getBoundingClientRect(); @@ -326,7 +339,7 @@ export async function openRedactor(file: File, viewer: HTMLElement): Promise !r.url.startsWith(page.url().split('/').slice(0, 3).join('/')))).toEqual([]); }); +test('scrolls the redact pages with a finger, and draws with one after “Draw boxes”', async ({ browser }) => { + const context = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true }); + const page = await context.newPage(); + await page.goto('/redact-pdf/'); + await page.locator('#file-input').setInputFiles(fixture('rich-text')); + const scroll = page.locator('.redact-scroll'); + await expect(page.locator('.redact-page').first().locator('canvas')).toBeVisible(); + const cdp = await context.newCDPSession(page); + const drag = async (from: { x: number; y: number }, to: { x: number; y: number }) => { + await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [from] }); + for (let i = 1; i <= 8; i++) + await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: from.x + ((to.x - from.x) * i) / 8, y: from.y + ((to.y - from.y) * i) / 8 }] }); + await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }); + }; + + // A swipe on the page scrolls (the pages or the whole site) and draws nothing. + const scrolled = () => scroll.evaluate((el) => el.scrollTop + scrollY); + const before = await scrolled(); + const area = (await scroll.boundingBox())!; + const middle = { x: area.x + area.width / 2, y: area.y + area.height * 0.7 }; + await drag(middle, { x: middle.x, y: middle.y - 300 }); + await expect.poll(scrolled).toBeGreaterThan(before + 100); + await expect(page.locator('.redact-box')).toHaveCount(0); + + // With "Draw boxes" on, the same finger drag marks an area. + const draw = page.getByRole('button', { name: 'Draw boxes' }); + await draw.tap(); + await expect(draw).toHaveAttribute('aria-pressed', 'true'); + const sheet = (await page.locator('.redact-overlay').first().boundingBox())!; + const visible = (await scroll.boundingBox())!; + const start = { x: sheet.x + 40, y: Math.max(sheet.y, visible.y) + 20 }; + await drag(start, { x: start.x + 150, y: start.y + 60 }); + await expect(page.locator('.redact-count')).toHaveText('Marked areas: 1'); + await context.close(); +}); + test('finds and marks personal details in one click, and leaves amounts and dates alone', async ({ page }) => { const { StandardFonts } = await import('pdf-lib'); const doc = await PDFDocument.create();