PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.5
Filter Everything — WordPress & WooCommerce Filters v1.9.5
1.9.7 1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 All 52 releases
filter-everything / assets / js / element-picker.js

element-picker.js in Filter Everything — WordPress & WooCommerce Filters 1.9.5, at assets/js/element-picker.js

275 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function ($) {
2 "use strict";
3
4 if (!window.ElementPicker?.ElementPicker) {
5 console.warn("Element Picker has not loaded yet. Please try again in a second.");
6 return;
7 }
8
9 const SelectorUtils = {
10
11 isUnique(selector) {
12 try {
13 return document.querySelectorAll(selector).length === 1;
14 } catch {
15 return false;
16 }
17 },
18
19 /**
20 * Returns the exact selector of one element relative to its parent.
21 * Format: tag.first-class:nth-of-type(N) or #id
22 * nth-of-type is always added if there are identical tags among siblings —
23 * this ensures precise targeting on any page.
24 */
25 getSegment(el) {
26 // ID — the most precise and always unique
27 if (el.id) {
28 return "#" + CSS.escape(el.id);
29 }
30
31 const parent = el.parentElement;
32 const tag = el.tagName.toLowerCase();
33
34 // Get the first class (the most stable identifier)
35 const rawClass = typeof el.className === "string"
36 ? el.className.trim().split(/\s+/)[0]
37 : null;
38 const cls = rawClass ? "." + CSS.escape(rawClass) : "";
39
40 // nth-of-type is calculated by tag (CSS specification)
41 const sameTagSiblings = parent
42 ? Array.from(parent.children).filter(s => s.tagName === el.tagName)
43 : [el];
44
45 const needsNth = sameTagSiblings.length > 1;
46 const nthIndex = sameTagSiblings.indexOf(el) + 1;
47 const nth = needsNth ? `:nth-of-type(${nthIndex})` : "";
48
49 return `${tag}${cls}${nth}`;
50 },
51
52 /**
53 * Builds the FULL path from <body> to the element.
54 * Each segment is precise — contains nth-of-type when needed.
55 * Result: guaranteed to be unique on any page.
56 */
57 buildFullPath(el) {
58 const segments = [];
59 let node = el;
60
61 while (node && node !== document.documentElement) {
62 if (node === document.body) {
63 segments.unshift("body");
64 break;
65 }
66 segments.unshift(this.getSegment(node));
67 node = node.parentElement;
68 }
69
70 return segments.join(" > ");
71 },
72
73 shortenPath(fullPath) {
74 const parts = fullPath.split(" > ");
75 const lastPart = parts[parts.length - 1];
76
77 //Try just the element itself
78 if (this.isUnique(lastPart)) {
79 return lastPart;
80 }
81
82 //Try element with simplified selectors (without nth-of-type if possible)
83 const simplifiedLast = this.simplifySegment(lastPart);
84 if (simplifiedLast !== lastPart && this.isUnique(simplifiedLast)) {
85 return simplifiedLast;
86 }
87
88 //Try adding parents one by one from bottom to top
89 for (let i = parts.length - 2; i >= 0; i--) {
90 const candidate = parts.slice(i).join(" > ");
91 if (this.isUnique(candidate)) {
92 return candidate;
93 }
94 }
95
96 // Fallback: full path — always unique
97 return fullPath;
98 },
99
100 /**
101 * Simplifies a segment by removing nth-of-type if the element is unique without it
102 */
103 simplifySegment(segment) {
104 // Remove nth-of-type
105 return segment.replace(/:nth-of-type\(\d+\)$/, '');
106 },
107 };
108
109 /**
110 * Main function.
111 * Returns { selector, type } — a guaranteed unique selector.
112 */
113 function generateUniqueSelector(element) {
114 // Divi: if click inside .et_pb_ajax_pagination_container — take the container
115 const isDivi = document.body.classList.contains("wp-theme-Divi") ||
116 document.body.classList.contains("theme-Divi");
117
118 if (isDivi) {
119 let node = element;
120 while (node && node !== document.body) {
121 if (node.classList?.contains("et_pb_ajax_pagination_container")) {
122 element = node;
123 break;
124 }
125 node = node.parentElement;
126 }
127 }
128
129 // Fast: unique ID
130 if (element.id) {
131 const sel = "#" + CSS.escape(element.id);
132 if (SelectorUtils.isUnique(sel)) {
133 return { selector: sel, type: "id" };
134 }
135 }
136
137 // Build full path and shorten
138 const full = SelectorUtils.buildFullPath(element);
139 const selector = SelectorUtils.shortenPath(full);
140
141 const type = selector.startsWith("#") ? "id"
142 : selector.includes(".") ? "class"
143 : "tag";
144
145 return { selector, type };
146 }
147
148 const UI = {
149 $infoPanel: null,
150
151 init() {
152 this.$infoPanel = $("#wpc-choose-selector");
153 },
154
155 showSelector(selectorData) {
156 $(".wpc-choose-block").hide();
157
158 const typeMap = {
159 id: { el: "#wpc-choose-id-css", display: "#wpc-chooses-html-id" },
160 class: { el: "#wpc-choose-class-css", display: "#wpc-chooses-html-css" },
161 tag: { el: "#wpc-choose-class-css", display: "#wpc-chooses-html-css" },
162 };
163
164 const config = typeMap[selectorData.type];
165 if (config) {
166 $(config.el).show();
167 $(config.display).val(selectorData.selector);
168 } else {
169 $("#wpc-choose-empty").show();
170 }
171
172 this.$infoPanel.show();
173 },
174
175 storeSelection(selectorData) {
176 this.$infoPanel.data("selectedElement", {
177 selector: selectorData.selector,
178 type: selectorData.type,
179 id: selectorData.type === "id" ? selectorData.selector : "",
180 className: selectorData.type !== "id" ? selectorData.selector : "",
181 });
182 },
183
184 resetPanel() {
185 this.$infoPanel.hide().removeData("selectedElement");
186 $(".wpc-choose-block").hide();
187 },
188
189 getSelection() {
190 return this.$infoPanel.data("selectedElement");
191 },
192
193 getSetId() {
194 return this.$infoPanel.data("setId");
195 },
196 };
197
198 function saveElementSelector(elementData, setId, $button) {
199 const elementSelector = elementData.id || elementData.className;
200
201 $button.prop("disabled", true).text(wpcElementPickerData.saving);
202
203 window.parent.postMessage(
204 { type: "SELECTOR_CHOSEN", elementSelector },
205 "*"
206 );
207 window.close();
208 }
209
210 function isInsideInfoPanel(element) {
211 return (
212 element.id === "wpc-choose-selector" ||
213 $(element).closest("#wpc-choose-selector").length > 0
214 );
215 }
216
217 function initPicker() {
218 return new window.ElementPicker.ElementPicker({
219 picking: true,
220
221 onClick(element, event) {
222 if (isInsideInfoPanel(element)) return;
223
224 const selectorData = generateUniqueSelector(element);
225 UI.storeSelection(selectorData);
226 UI.showSelector(selectorData);
227
228 event.stopPropagation();
229 this.stopPicking();
230 $("#wpc-choose-another-selector").show();
231 },
232
233 onTargetChange(element) {
234 if (isInsideInfoPanel(element)) return;
235
236 const selectorData = generateUniqueSelector(element);
237 UI.storeSelection(selectorData);
238 UI.showSelector(selectorData);
239 },
240
241 overlayDrawer() {
242 const overlay = document.createElement("div");
243 Object.assign(overlay.style, {
244 width: "100%",
245 height: "100%",
246 background: "rgba(102, 126, 234, 0.2)",
247 border: "3px solid #667eea",
248 boxSizing: "border-box",
249 });
250 return overlay;
251 },
252 });
253 }
254
255 UI.init();
256 initPicker();
257
258 $(document).on("click", "#wpc-choose-another-selector", function () {
259 $("#wpc-choose-another-selector").hide();
260 initPicker();
261 });
262
263 $(document).on("click", "#wpc-choose-save-btn", function (e) {
264 e.preventDefault();
265
266 const elementData = UI.getSelection();
267 if (!elementData) {
268 alert(wpcElementPickerData.no_element_selected);
269 return;
270 }
271
272 saveElementSelector(elementData, UI.getSetId(), $(this));
273 });
274
275 })(jQuery);