feat: numbered element chips for picker, multi-select support
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 3s
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 3s
Replace the old single-snippet textarea append with a chip-based UI: - Each picked element gets a monotonically increasing number [1], [2], … - A chip appears below the textarea: [N] selector ✕ - The token [element N] is inserted at the cursor position in the textarea - Multiple elements can be picked in one session - Chips can be removed (number is never re-used or re-ordered) - On submit, element snippets are appended to the issue body as named reference blocks so the HTML context is still captured Closes #6
This commit is contained in:
parent
7a593d082a
commit
1afd2a0e4f
83
server.js
83
server.js
@ -183,10 +183,17 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
.fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem;
|
.fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem;
|
||||||
padding:.4rem .7rem; border-radius:6px; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
padding:.4rem .7rem; border-radius:6px; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
||||||
.fb-picker-btn:hover { background:#223322; }
|
.fb-picker-btn:hover { background:#223322; }
|
||||||
.fb-picker-hint { color:#444; font-size:.75rem; }
|
/* Picked-elements chip list */
|
||||||
.fb-snippet-preview { background:#0a0a0a; border:1px solid #2a2a2a; border-radius:6px;
|
#_fb-picked-list { display:flex; flex-wrap:wrap; gap:.4rem; }
|
||||||
padding:.5rem .7rem; font-size:.72rem; color:#7c9ef5; font-family:monospace;
|
#_fb-picked-list:empty { display:none; }
|
||||||
white-space:pre-wrap; word-break:break-all; max-height:80px; overflow-y:auto; }
|
.fb-picked-chip { display:inline-flex; align-items:center; gap:.35rem; background:#0d1f0d;
|
||||||
|
border:1px solid #2a4a2a; border-radius:6px; padding:.28rem .55rem; font-size:.75rem;
|
||||||
|
color:#6fcf6f; font-family:monospace; }
|
||||||
|
.fb-chip-num { color:#4a9f4a; font-weight:700; }
|
||||||
|
.fb-chip-label { color:#aaa; max-width:140px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
.fb-chip-remove { background:none; border:none; color:#555; cursor:pointer; font-size:.7rem;
|
||||||
|
padding:0; line-height:1; }
|
||||||
|
.fb-chip-remove:hover { color:#f57c7c; }
|
||||||
/* Picker overlay */
|
/* Picker overlay */
|
||||||
#_fb-picker-shield { display:none; position:fixed; inset:0; z-index:199999; cursor:crosshair; }
|
#_fb-picker-shield { display:none; position:fixed; inset:0; z-index:199999; cursor:crosshair; }
|
||||||
#_fb-picker-shield.active { display:block; }
|
#_fb-picker-shield.active { display:block; }
|
||||||
@ -240,11 +247,8 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="fb-picker-row">
|
<div class="fb-picker-row">
|
||||||
<button class="fb-picker-btn" id="_fb-pick-btn">🎯 Pick element</button>
|
<button class="fb-picker-btn" id="_fb-pick-btn">🎯 Pick element</button>
|
||||||
<span class="fb-picker-hint" id="_fb-pick-hint">Click to reference a page element</span>
|
|
||||||
</div>
|
|
||||||
<div id="_fb-snippet-wrap" style="display:none;">
|
|
||||||
<div class="fb-snippet-preview" id="_fb-snippet"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="_fb-picked-list"></div>
|
||||||
<button class="fb-btn" id="_fb-submit">Submit Issue</button>
|
<button class="fb-btn" id="_fb-submit">Submit Issue</button>
|
||||||
<div class="fb-status" id="_fb-status"></div>
|
<div class="fb-status" id="_fb-status"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -302,7 +306,8 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
// ── Picker mode ───────────────────────────────────────────────────────────
|
// ── Picker mode ───────────────────────────────────────────────────────────
|
||||||
var pickerActive = false;
|
var pickerActive = false;
|
||||||
var pickerHighlighted = null;
|
var pickerHighlighted = null;
|
||||||
var capturedSnippet = '';
|
var nextPickNum = 1;
|
||||||
|
var pickedElements = {}; // { [num]: { selector, snippet } }
|
||||||
|
|
||||||
function getSelector(el) {
|
function getSelector(el) {
|
||||||
if (!el || el === document.body) return 'body';
|
if (!el || el === document.body) return 'body';
|
||||||
@ -332,7 +337,30 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
toast.classList.add('active');
|
toast.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function deactivatePicker(snippet) {
|
function insertAtCursor(ta, text) {
|
||||||
|
var start = ta.selectionStart != null ? ta.selectionStart : ta.value.length;
|
||||||
|
var end = ta.selectionEnd != null ? ta.selectionEnd : ta.value.length;
|
||||||
|
ta.value = ta.value.slice(0, start) + text + ta.value.slice(end);
|
||||||
|
ta.selectionStart = ta.selectionEnd = start + text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPickedChip(num, selector, snippet) {
|
||||||
|
var list = document.getElementById('_fb-picked-list');
|
||||||
|
var chip = document.createElement('span');
|
||||||
|
chip.className = 'fb-picked-chip';
|
||||||
|
chip.dataset.num = num;
|
||||||
|
chip.innerHTML =
|
||||||
|
'<span class="fb-chip-num">[' + num + ']</span>' +
|
||||||
|
'<span class="fb-chip-label">' + esc(selector) + '</span>' +
|
||||||
|
'<button class="fb-chip-remove" title="Remove">✕</button>';
|
||||||
|
chip.querySelector('.fb-chip-remove').addEventListener('click', function() {
|
||||||
|
delete pickedElements[num];
|
||||||
|
chip.remove();
|
||||||
|
});
|
||||||
|
list.appendChild(chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivatePicker(snippet, selector) {
|
||||||
pickerActive = false;
|
pickerActive = false;
|
||||||
shield.classList.remove('active');
|
shield.classList.remove('active');
|
||||||
toast.classList.remove('active');
|
toast.classList.remove('active');
|
||||||
@ -342,14 +370,14 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
}
|
}
|
||||||
overlay.classList.add('open');
|
overlay.classList.add('open');
|
||||||
if (snippet) {
|
if (snippet) {
|
||||||
capturedSnippet = snippet;
|
var num = nextPickNum++;
|
||||||
document.getElementById('_fb-snippet').textContent = snippet;
|
pickedElements[num] = { selector: selector, snippet: snippet };
|
||||||
document.getElementById('_fb-snippet-wrap').style.display = '';
|
addPickedChip(num, selector, snippet);
|
||||||
document.getElementById('_fb-pick-hint').textContent = '✓ Element captured — you can pick again';
|
// Insert reference token at cursor in the textarea
|
||||||
// Append to description
|
|
||||||
var desc = document.getElementById('_fb-desc');
|
var desc = document.getElementById('_fb-desc');
|
||||||
var tag = '\\n\\n**Picked element:**\\n\`\`\`html\\n' + snippet + '\\n\`\`\`';
|
var token = '[element ' + num + ']';
|
||||||
if (!desc.value.includes(tag)) desc.value += tag;
|
insertAtCursor(desc, token);
|
||||||
|
desc.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,15 +411,15 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
shield.style.display = '';
|
shield.style.display = '';
|
||||||
if (el && !el.closest('#_fb-picker-toast')) {
|
if (el && !el.closest('#_fb-picker-toast')) {
|
||||||
el.classList.remove('_fb-hover-highlight');
|
el.classList.remove('_fb-hover-highlight');
|
||||||
deactivatePicker(getSnippet(el));
|
deactivatePicker(getSnippet(el), getSelector(el));
|
||||||
} else {
|
} else {
|
||||||
deactivatePicker(null);
|
deactivatePicker(null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Esc cancels picker
|
// Esc cancels picker
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Escape' && pickerActive) deactivatePicker(null);
|
if (e.key === 'Escape' && pickerActive) deactivatePicker(null, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Submit ────────────────────────────────────────────────────────────────
|
// ── Submit ────────────────────────────────────────────────────────────────
|
||||||
@ -403,9 +431,16 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
var btn = document.getElementById('_fb-submit');
|
var btn = document.getElementById('_fb-submit');
|
||||||
if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; }
|
if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; }
|
||||||
btn.disabled = true; status.style.color='#888'; status.textContent='Submitting…';
|
btn.disabled = true; status.style.color='#888'; status.textContent='Submitting…';
|
||||||
|
// Append element snippets as a reference block
|
||||||
|
var snippetBlock = '';
|
||||||
|
Object.keys(pickedElements).forEach(function(num) {
|
||||||
|
var pe = pickedElements[num];
|
||||||
|
snippetBlock += '\\n\\n**[element ' + num + ']** `' + pe.selector + '`\\n\`\`\`html\\n' + pe.snippet + '\\n\`\`\`';
|
||||||
|
});
|
||||||
|
var fullBody = body + snippetBlock;
|
||||||
fetch(SELF + '/api/feedback', {
|
fetch(SELF + '/api/feedback', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ title, body, repo: cfg.repo, source_url: window.location.href, assignee: assignee || undefined })
|
body: JSON.stringify({ title, body: fullBody, repo: cfg.repo, source_url: window.location.href, assignee: assignee || undefined })
|
||||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
status.style.color='#6fcf6f';
|
status.style.color='#6fcf6f';
|
||||||
@ -413,9 +448,9 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
document.getElementById('_fb-title').value = '';
|
document.getElementById('_fb-title').value = '';
|
||||||
document.getElementById('_fb-desc').value = '';
|
document.getElementById('_fb-desc').value = '';
|
||||||
document.getElementById('_fb-assignee').value = '';
|
document.getElementById('_fb-assignee').value = '';
|
||||||
document.getElementById('_fb-snippet-wrap').style.display = 'none';
|
document.getElementById('_fb-picked-list').innerHTML = '';
|
||||||
document.getElementById('_fb-pick-hint').textContent = 'Click to reference a page element';
|
nextPickNum = 1;
|
||||||
capturedSnippet = '';
|
pickedElements = {};
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
setTimeout(function() { status.textContent=''; }, 3000);
|
setTimeout(function() { status.textContent=''; }, 3000);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user