pi-ele-picker 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/client/picker.js +88 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,10 +110,10 @@ The launcher starts in the **bottom-right** corner and stays out of the way:
|
|
|
110
110
|
Vite reloads the page every time you save a file, so the composer is mirrored into `sessionStorage` and re-attached on the next load:
|
|
111
111
|
|
|
112
112
|
- The description and any attached screenshots come back exactly as they were.
|
|
113
|
+
- The captured element is **snapshotted at pick time and persisted with the draft**, so the element data survives the node being unmounted, an SPA route change, or the reload itself.
|
|
113
114
|
- The captured element is **re-resolved from its selector**, so after an HMR update you land straight back in the panel on the same element — no re-pick.
|
|
114
115
|
- Closing the panel with **✕** also suppresses the automatic reopen; the draft stays reachable through the launcher's hover.
|
|
115
|
-
-
|
|
116
|
-
- Sending is refused while the panel points at an element that has left the page, so a stale pick can never ship a description of nothing. A replaced node under the same selector (the usual HMR case) is picked up automatically instead.
|
|
116
|
+
- **Sending never depends on the element still being on the page.** Pick a button, go copy something from another route, then send — the description goes out with the element as it was when you picked it. A live read is used instead while you are still on the pick-time page, so an HMR swap of the same node is described in its current state; after a route change the snapshot wins, so a lookalike behind the same selector cannot be mistaken for your pick. When a snapshot was used, the status line says so.
|
|
117
117
|
- The draft is per tab and per path (`sessionStorage`, not `localStorage`), and one older than 24h is discarded. A successful send clears it.
|
|
118
118
|
- Screenshots that exceed the ~5MB `sessionStorage` quota are dropped from the draft with a notice rather than silently lost; the description is always kept.
|
|
119
119
|
|
package/client/picker.js
CHANGED
|
@@ -155,6 +155,12 @@
|
|
|
155
155
|
// The selector `target` was captured through. Kept so the draft can be
|
|
156
156
|
// re-attached after a reload replaces the DOM node `target` points at.
|
|
157
157
|
var targetSelector = null;
|
|
158
|
+
// Everything about the target captured at pick time. Sending must not depend on
|
|
159
|
+
// the node still being on the page — see elementPayload().
|
|
160
|
+
var targetSnapshot = null;
|
|
161
|
+
// The URL the target was picked on. A selector only identifies an element within
|
|
162
|
+
// one document: after a route change the same selector may hit a lookalike.
|
|
163
|
+
var targetUrl = null;
|
|
158
164
|
var shots = [];
|
|
159
165
|
// Set when a pick commits on pointerdown; swallows the follow-up click so the
|
|
160
166
|
// page (links, buttons) never reacts to the click that finished the pick.
|
|
@@ -491,12 +497,20 @@
|
|
|
491
497
|
}
|
|
492
498
|
}
|
|
493
499
|
|
|
494
|
-
/**
|
|
500
|
+
/**
|
|
501
|
+
* Re-show the panel for the element already captured — no re-pick, no overwrite.
|
|
502
|
+
*
|
|
503
|
+
* A snapshot is enough to reopen: the developer may have navigated away from the
|
|
504
|
+
* page that element lives on, and losing access to their own draft over that
|
|
505
|
+
* would be the wrong trade. The highlight is simply skipped when there is
|
|
506
|
+
* nothing on screen to outline.
|
|
507
|
+
*/
|
|
495
508
|
function openPanel() {
|
|
496
509
|
var el = resolveTarget();
|
|
497
|
-
if (!el) return false;
|
|
510
|
+
if (!el && !targetSnapshot) return false;
|
|
498
511
|
panel.classList.add('open');
|
|
499
|
-
showHighlight(el);
|
|
512
|
+
if (el) showHighlight(el);
|
|
513
|
+
else hideHighlight();
|
|
500
514
|
positionPanel();
|
|
501
515
|
return true;
|
|
502
516
|
}
|
|
@@ -504,6 +518,8 @@
|
|
|
504
518
|
function select(el) {
|
|
505
519
|
target = el;
|
|
506
520
|
targetSelector = cssPath(el);
|
|
521
|
+
targetSnapshot = describe(el);
|
|
522
|
+
targetUrl = location.href;
|
|
507
523
|
// A new pick re-arms the draft: the user is working again, so a reload must
|
|
508
524
|
// bring the panel back even if they closed it earlier.
|
|
509
525
|
panelDismissed = false;
|
|
@@ -513,6 +529,14 @@
|
|
|
513
529
|
taEl.focus();
|
|
514
530
|
}
|
|
515
531
|
|
|
532
|
+
/**
|
|
533
|
+
* The element description handed to the bridge is chosen in send(), because the
|
|
534
|
+
* choice between a live read and the snapshot has to be reported to the user.
|
|
535
|
+
* Describing a detached node is never an option: `getComputedStyle()` and
|
|
536
|
+
* `getBoundingClientRect()` return nothing useful once it leaves the document, so
|
|
537
|
+
* the fallback has to be the captured value, not a fresh read.
|
|
538
|
+
*/
|
|
539
|
+
|
|
516
540
|
function vueLine(el) {
|
|
517
541
|
var v = vueInfo(el);
|
|
518
542
|
if (v.componentFile) return '<br>组件: <code>' + escapeHtml((v.componentName ? v.componentName + ' ' : '') + v.componentFile) + '</code>';
|
|
@@ -729,10 +753,15 @@
|
|
|
729
753
|
var rec = {
|
|
730
754
|
selector: targetSelector,
|
|
731
755
|
text: taEl.value,
|
|
756
|
+
// The element as it was when it was picked. Persisted because the reload
|
|
757
|
+
// that restores the draft is often the same one that removed the node —
|
|
758
|
+
// without this the send below could only go out with no element at all.
|
|
759
|
+
element: targetSnapshot,
|
|
760
|
+
url: targetUrl,
|
|
732
761
|
dismissed: panelDismissed,
|
|
733
762
|
savedAt: Date.now(),
|
|
734
763
|
};
|
|
735
|
-
if (!rec.text && !rec.selector) {
|
|
764
|
+
if (!rec.text && !rec.selector && !rec.element) {
|
|
736
765
|
// Nothing worth restoring — do not resurrect an empty panel on reload.
|
|
737
766
|
clearDraft();
|
|
738
767
|
return;
|
|
@@ -839,21 +868,26 @@
|
|
|
839
868
|
}
|
|
840
869
|
}
|
|
841
870
|
|
|
842
|
-
if (rec.selector)
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
871
|
+
if (rec.selector) targetSelector = String(rec.selector);
|
|
872
|
+
if (rec.url) targetUrl = String(rec.url);
|
|
873
|
+
if (rec.element && typeof rec.element === 'object') targetSnapshot = rec.element;
|
|
874
|
+
|
|
875
|
+
var el = targetSelector ? resolveTarget() : null;
|
|
876
|
+
if (el) {
|
|
877
|
+
metaEl.innerHTML = '<code>' + escapeHtml(targetSelector) + '</code>' + vueLine(el);
|
|
878
|
+
} else if (targetSnapshot) {
|
|
879
|
+
// Still sendable: the snapshot carries everything the bridge needs.
|
|
880
|
+
metaEl.innerHTML =
|
|
881
|
+
'<code>' + escapeHtml(targetSelector || '') + '</code><br>' +
|
|
882
|
+
'上次选中的元素已不在页面中,发送时会带上抓取时的快照';
|
|
847
883
|
}
|
|
848
884
|
|
|
849
885
|
if (rec.dismissed) return;
|
|
850
|
-
if (
|
|
851
|
-
openPanel();
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
886
|
+
if (openPanel()) return;
|
|
854
887
|
if (!taEl.value && !shots.length) return;
|
|
855
|
-
//
|
|
856
|
-
// visibly still there, and let the next pick attach
|
|
888
|
+
// No element at all (not even a snapshot) but there are words: show the
|
|
889
|
+
// composer so the draft is visibly still there, and let the next pick attach
|
|
890
|
+
// it to a new element.
|
|
857
891
|
panel.classList.add('open');
|
|
858
892
|
metaEl.textContent = '上次选中的元素已不在页面中,请重新点击选择';
|
|
859
893
|
positionPanel();
|
|
@@ -875,14 +909,16 @@
|
|
|
875
909
|
}
|
|
876
910
|
|
|
877
911
|
/** POST the payload to the bridge. Never rejects; status 0 means the fetch itself failed. */
|
|
878
|
-
function deliver(c, submit) {
|
|
912
|
+
function deliver(c, submit, element) {
|
|
879
913
|
var payload = {
|
|
880
914
|
text: taEl.value,
|
|
881
915
|
images: shots.map(function (d, i) {
|
|
882
916
|
return { name: 'shot-' + i + '.png', dataUrl: d };
|
|
883
917
|
}),
|
|
884
|
-
element:
|
|
885
|
-
|
|
918
|
+
element: element,
|
|
919
|
+
// The page the element lives on, which is not necessarily the page that is
|
|
920
|
+
// open right now.
|
|
921
|
+
url: targetUrl || location.href,
|
|
886
922
|
title: document.title,
|
|
887
923
|
submit: !!submit,
|
|
888
924
|
};
|
|
@@ -919,12 +955,32 @@
|
|
|
919
955
|
}
|
|
920
956
|
|
|
921
957
|
function send(submit) {
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
|
|
958
|
+
// Deliberately NOT gated on the element still being on the page. Picking an
|
|
959
|
+
// element and then wandering off to another route to copy something is a
|
|
960
|
+
// normal way to work, and blocking the send there throws away the words the
|
|
961
|
+
// developer typed.
|
|
962
|
+
//
|
|
963
|
+
// Two sources for the element, and the wrong one silently corrupts the payload:
|
|
964
|
+
// the snapshot captured at pick time (a plain value, so it survives an unmount,
|
|
965
|
+
// an SPA route change or a reload), and a live read of whatever the selector
|
|
966
|
+
// resolves to now. A live read wins only while we are still on the pick-time
|
|
967
|
+
// URL — an HMR swap of the same element is the case that needs it, while a
|
|
968
|
+
// route change can put an unrelated lookalike behind the same selector.
|
|
969
|
+
var live = resolveTarget();
|
|
970
|
+
var samePage = targetUrl === null || location.href === targetUrl;
|
|
971
|
+
var element = live && samePage ? describe(live) : targetSnapshot;
|
|
972
|
+
if (element) targetSnapshot = element;
|
|
973
|
+
|
|
974
|
+
if (!element && !taEl.value.trim()) {
|
|
975
|
+
setStatus('请先点击页面元素,或写下要改什么', 'err');
|
|
926
976
|
return;
|
|
927
977
|
}
|
|
978
|
+
|
|
979
|
+
// Only ever affects the status line, never whether the send happens.
|
|
980
|
+
var note = '';
|
|
981
|
+
if (element && !live) note = '(元素已不在页面中,已附抓取时的快照)';
|
|
982
|
+
else if (element && !samePage) note = '(页面已切换,已附抓取时的元素快照)';
|
|
983
|
+
|
|
928
984
|
sendBtn.disabled = true;
|
|
929
985
|
sendNowBtn.disabled = true;
|
|
930
986
|
setStatus(submit ? '直接发送中…' : '写入中…');
|
|
@@ -932,7 +988,7 @@
|
|
|
932
988
|
function attempt(forceFresh) {
|
|
933
989
|
return loadConfig(forceFresh).then(function (c) {
|
|
934
990
|
if (!c || !c.port) return { status: 0, body: null, noBridge: true };
|
|
935
|
-
return deliver(c, submit);
|
|
991
|
+
return deliver(c, submit, element);
|
|
936
992
|
});
|
|
937
993
|
}
|
|
938
994
|
|
|
@@ -950,10 +1006,17 @@
|
|
|
950
1006
|
return;
|
|
951
1007
|
}
|
|
952
1008
|
if (out.body && out.body.ok) {
|
|
1009
|
+
// Say which element went out, so a stale pick is visible rather than silent.
|
|
953
1010
|
if (out.body.submitted) {
|
|
954
|
-
setStatus(
|
|
1011
|
+
setStatus(
|
|
1012
|
+
(out.body.queued ? '已发送 ✓(agent 忙碌,已排队为后续任务)' : '已直接发送给 pi 并开始执行 ✓') + note,
|
|
1013
|
+
'ok'
|
|
1014
|
+
);
|
|
955
1015
|
} else {
|
|
956
|
-
setStatus(
|
|
1016
|
+
setStatus(
|
|
1017
|
+
(out.body.inserted ? '已写入 pi 输入框 ✓(去终端按回车发送)' : '已发送给 pi ✓') + note,
|
|
1018
|
+
'ok'
|
|
1019
|
+
);
|
|
957
1020
|
}
|
|
958
1021
|
taEl.value = '';
|
|
959
1022
|
shots = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ele-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Pick an element in your running page, describe the change, and send it to a pi session. Ships a Vite dev plugin (browser picker + discovery endpoint) and the matching pi bridge extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|