diff --git a/content.js b/content.js index f5f7838..7b87cec 100644 --- a/content.js +++ b/content.js @@ -230,70 +230,63 @@ const questions = []; const seen = new Set(); - // Strategy 1: Look inside known modal containers - const containers = document.querySelectorAll( - '.artdeco-modal, .jobs-easy-apply-modal, #artdeco-modal-outlet, ' + - '.jobs-easy-apply-content, .jobs-easy-apply-form-section__grouping, ' + - '[data-test-modal], [role="dialog"]' + // Find the Easy Apply modal using real LinkedIn selectors + const modal = document.querySelector( + '[role="dialog"].jobs-easy-apply-modal, .artdeco-modal.jobs-easy-apply-modal, ' + + '.artdeco-modal, [role="dialog"], #artdeco-modal-outlet' + ); + const searchRoot = modal || document.body; + + // LinkedIn wraps each form field in .fb-dash-form-element or [data-test-form-element] + const formElements = searchRoot.querySelectorAll( + '.fb-dash-form-element, [data-test-form-element]' ); - // Strategy 2: If no modal found, scan the full page - const searchRoots = containers.length > 0 ? containers : [document.body]; + formElements.forEach(el => { + const label = el.querySelector('label'); + if (!label) return; - searchRoots.forEach(container => { - // Scan labels, legends, spans that look like question text - const candidates = container.querySelectorAll( - 'label, legend, .fb-dash-form-element__label, ' + - '.jobs-easy-apply-form-element__label, ' + - 'span.t-14, span.t-bold' - ); + // Prefer the aria-hidden span text to avoid duplication from visually-hidden span + const ariaSpan = label.querySelector('span[aria-hidden="true"]'); + let text = (ariaSpan ? ariaSpan.textContent : label.textContent) + .replace(/\*/g, '') + .replace(/required/gi, '') + .replace(/\n/g, ' ') + .replace(/\s+/g, ' ') + .trim(); - candidates.forEach(el => { - let text = el.textContent.replace(/\*/g, '').replace(/required/gi, '').trim(); - // Remove "Select an option" and similar noise - text = text.replace(/Select an option/gi, '').replace(/Show less|Show more/gi, '').trim(); + if (!text || text.length < 5 || text.length > 300 || seen.has(text.toLowerCase())) return; + seen.add(text.toLowerCase()); - if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return; - - // Only accept text that looks like a question or form label - const isQuestion = ( - text.endsWith('?') || - /^(how many|what|do you|are you|have you|will you|can you|would you|is your|describe|tell us)/i.test(text) || - /salary|experience|education|degree|visa|relocat|notice period|start date|certif/i.test(text) - ); + // Detect input type from this form element + let inputType = 'text'; + if (el.querySelector('select')) inputType = 'select'; + else if (el.querySelector('input[type="radio"]')) inputType = 'radio'; + else if (el.querySelector('textarea')) inputType = 'textarea'; + else if (el.querySelector('input[type="checkbox"]')) inputType = 'checkbox'; - if (!isQuestion) return; - - seen.add(text.toLowerCase()); - - // Detect input type from nearest form element - let inputType = 'text'; - const parent = el.closest('.fb-dash-form-element, .jobs-easy-apply-form-section__grouping, div') || el.parentElement; - if (parent) { - if (parent.querySelector('select')) inputType = 'select'; - else if (parent.querySelector('input[type="radio"]')) inputType = 'radio'; - else if (parent.querySelector('textarea')) inputType = 'textarea'; - else if (parent.querySelector('input[type="checkbox"]')) inputType = 'checkbox'; - else if (parent.querySelector('input[type="number"]')) inputType = 'number'; - } - - questions.push({ question: text, type: inputType }); - }); + questions.push({ question: text, type: inputType }); }); - // Strategy 3: Last resort — scan ALL visible text for question patterns + // Fallback: if the precise approach found nothing, scan broader if (questions.length === 0) { - const allSpans = document.querySelectorAll('span, label, legend, p'); - allSpans.forEach(el => { - const text = el.textContent.replace(/\*/g, '').trim(); - if (text && text.endsWith('?') && text.length > 10 && text.length < 200 && !seen.has(text.toLowerCase())) { - // Make sure this is a visible form question, not random page text - const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0 && rect.top > 0 && rect.top < window.innerHeight) { - seen.add(text.toLowerCase()); - questions.push({ question: text, type: 'text' }); - } - } + const allLabels = searchRoot.querySelectorAll('label'); + allLabels.forEach(label => { + const ariaSpan = label.querySelector('span[aria-hidden="true"]'); + let text = (ariaSpan ? ariaSpan.textContent : label.textContent) + .replace(/\*/g, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); + + if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return; + + const looksLikeQuestion = ( + text.endsWith('?') || + /^(how many|what|do you|are you|have you|will you|describe|tell us)/i.test(text) || + /salary|experience|education|degree|visa|relocat|notice|certif/i.test(text) + ); + if (!looksLikeQuestion) return; + + seen.add(text.toLowerCase()); + questions.push({ question: text, type: 'text' }); }); } @@ -812,13 +805,23 @@ Brief honest assessment of this opportunity for my profile` const endIdx = text.lastIndexOf('}'); if (startIdx !== -1 && endIdx !== -1) { const parsed = JSON.parse(text.substring(startIdx, endIdx + 1)); - let html = '