rails-markup 1.4.3 → 1.4.4
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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/rails_markup/toolbar/00-core.js +68 -0
- data/app/assets/javascripts/rails_markup/toolbar/10-styles.js +82 -0
- data/app/assets/javascripts/rails_markup/toolbar/20-dom.js +95 -0
- data/app/assets/javascripts/rails_markup/toolbar/30-menu.js +176 -0
- data/app/assets/javascripts/rails_markup/toolbar/40-events.js +350 -0
- data/app/assets/javascripts/rails_markup/toolbar/50-sync.js +1087 -0
- data/app/assets/javascripts/rails_markup/toolbar/60-render.js +675 -0
- data/app/assets/javascripts/rails_markup/toolbar/90-init.js +91 -0
- data/app/views/rails_markup/shared/_toolbar.html.erb +1 -1
- data/lib/rails_markup/engine.rb +0 -7
- data/lib/rails_markup/toolbar_source.rb +24 -0
- data/lib/rails_markup/version.rb +1 -1
- data/lib/rails_markup.rb +1 -0
- metadata +10 -2
- data/app/assets/javascripts/rails_markup/toolbar.js +0 -2779
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
Object.assign(RailsMarkupToolbar, {
|
|
2
|
+
_bindEvents() {
|
|
3
|
+
const self = this;
|
|
4
|
+
document.getElementById("rm-fab").addEventListener("click", () => self.toggleMode());
|
|
5
|
+
document.getElementById("rm-panel-toggle").addEventListener("click", () => self.togglePanel());
|
|
6
|
+
document.getElementById("rm-panel-close").addEventListener("click", () => self.togglePanel());
|
|
7
|
+
document.getElementById("rm-btn-cancel").addEventListener("click", () => self._closePopup());
|
|
8
|
+
document.getElementById("rm-btn-submit").addEventListener("click", (e) => self.submitAnnotation(e));
|
|
9
|
+
document.getElementById("rm-popup-input").addEventListener("input", () => self._updateCharCount());
|
|
10
|
+
document.getElementById("rm-filter-chips").addEventListener("click", (e) => {
|
|
11
|
+
const chip = e.target.closest("[data-filter]");
|
|
12
|
+
if (chip) self._filterAnnotations(chip.dataset.filter);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Custom menus (intent/severity/status) — button+menu, never native selects
|
|
16
|
+
// so host FormSelect/Select2 enhancers cannot rewrite our DOM (#4).
|
|
17
|
+
this.root.addEventListener("click", (e) => {
|
|
18
|
+
const option = e.target.closest(".rm-menu-option");
|
|
19
|
+
if (option) {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
e.stopPropagation();
|
|
22
|
+
this._selectMenuOption(option);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const btn = e.target.closest(".rm-menu-btn");
|
|
26
|
+
if (btn) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
e.stopPropagation();
|
|
29
|
+
const menu = this._menuForElement(btn);
|
|
30
|
+
if (!menu || !this.root.contains(menu)) return;
|
|
31
|
+
this._toggleMenu(menu);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!this._menuForElement(e.target)) this._closeAllMenus();
|
|
35
|
+
});
|
|
36
|
+
if (!this._boundMenuDocClick) {
|
|
37
|
+
this._boundMenuDocClick = (e) => {
|
|
38
|
+
if (!this.root || this._menuForElement(e.target)) return;
|
|
39
|
+
this._closeAllMenus();
|
|
40
|
+
};
|
|
41
|
+
document.addEventListener("click", this._boundMenuDocClick);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Event delegation for cards (status change, edit, delete, or click scrolls to element)
|
|
45
|
+
const panelList = document.getElementById("rm-panel-list");
|
|
46
|
+
if (!this._boundMenuViewportChange) {
|
|
47
|
+
this._boundMenuViewportChange = () => self._closeAllMenus();
|
|
48
|
+
window.addEventListener("resize", this._boundMenuViewportChange);
|
|
49
|
+
window.addEventListener("scroll", this._boundMenuViewportChange, { passive: true });
|
|
50
|
+
}
|
|
51
|
+
panelList.addEventListener("scroll", this._boundMenuViewportChange, { passive: true });
|
|
52
|
+
panelList.addEventListener("click", (e) => {
|
|
53
|
+
const retryBtn = e.target.closest("[data-retry-client-id]");
|
|
54
|
+
if (retryBtn) {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
self._retrySync(retryBtn.dataset.retryClientId);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Status menu — leave bubbling for the root menu handler; don't scroll.
|
|
60
|
+
if (e.target.closest(".rm-menu")) return;
|
|
61
|
+
// Edit button
|
|
62
|
+
const editBtn = e.target.closest("[data-edit-id]");
|
|
63
|
+
if (editBtn) {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
const id = parseInt(editBtn.dataset.editId, 10);
|
|
66
|
+
self._editAnnotation(id);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Delete button
|
|
70
|
+
const deleteBtn = e.target.closest("[data-delete-id]");
|
|
71
|
+
if (deleteBtn) {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
const id = parseInt(deleteBtn.dataset.deleteId, 10);
|
|
74
|
+
self._deleteAnnotation(id);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Card click scrolls to element
|
|
78
|
+
const card = e.target.closest("[data-card-id]");
|
|
79
|
+
if (!card) return;
|
|
80
|
+
const id = parseInt(card.dataset.cardId, 10);
|
|
81
|
+
const annotation = self.annotations.find(a => a.id === id);
|
|
82
|
+
if (!annotation) return;
|
|
83
|
+
const el = self._findElement(annotation);
|
|
84
|
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Event delegation for pins (click opens panel + scrolls to card)
|
|
88
|
+
document.getElementById("rm-pins-container").addEventListener("click", (e) => {
|
|
89
|
+
const pin = e.target.closest("[data-pin-id]");
|
|
90
|
+
if (!pin) return;
|
|
91
|
+
const panel = document.getElementById("rm-panel");
|
|
92
|
+
if (panel.style.display !== "flex") self.togglePanel();
|
|
93
|
+
const card = document.querySelector('[data-card-id="' + pin.dataset.pinId + '"]');
|
|
94
|
+
if (card) card.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this._boundMouseMove = (e) => self._handleMouseMove(e);
|
|
98
|
+
this._boundMouseDown = (e) => self._handleMouseDown(e);
|
|
99
|
+
this._boundMouseUp = (e) => self._handleMouseUp(e);
|
|
100
|
+
this._boundClick = (e) => self._handleClick(e);
|
|
101
|
+
if (!this._boundKeyDown) {
|
|
102
|
+
this._boundKeyDown = (e) => self._handleKeyDown(e);
|
|
103
|
+
document.addEventListener("keydown", this._boundKeyDown, true);
|
|
104
|
+
}
|
|
105
|
+
this._boundTouchStart = (e) => { if (self.active && e.touches[0]) { const t = e.touches[0]; const el = document.elementFromPoint(t.clientX, t.clientY); if (el && !self._isToolbar(el) && e.cancelable) e.preventDefault(); self._handleMouseDown({ clientX: t.clientX, clientY: t.clientY }); } };
|
|
106
|
+
this._boundTouchEnd = (e) => { if (self.active && e.changedTouches[0]) { const t = e.changedTouches[0]; const el = document.elementFromPoint(t.clientX, t.clientY); if (el && !self._isToolbar(el)) { e.preventDefault(); self._handleMouseUp({ clientX: t.clientX, clientY: t.clientY, preventDefault(){}, stopPropagation(){} }); } } };
|
|
107
|
+
|
|
108
|
+
// Turbo Frames — partial DOM update, reposition pins
|
|
109
|
+
if (!this._boundTurboFrame) {
|
|
110
|
+
this._boundTurboFrame = () => self._onTurboFrameRender();
|
|
111
|
+
document.addEventListener("turbo:frame-render", this._boundTurboFrame);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
_onTurboNavigate() {
|
|
115
|
+
const newPageUrl = this._pageUrl();
|
|
116
|
+
if (newPageUrl === this._currentPageUrl) return; // same page (anchor change, etc.)
|
|
117
|
+
this._currentPathname = window.location.pathname;
|
|
118
|
+
this._currentPageUrl = newPageUrl;
|
|
119
|
+
|
|
120
|
+
// Deactivate crosshair mode
|
|
121
|
+
this._deactivateMode();
|
|
122
|
+
|
|
123
|
+
// Close popup
|
|
124
|
+
const popup = document.getElementById("rm-popup");
|
|
125
|
+
if (popup && popup.style.display === "block") this._closePopup();
|
|
126
|
+
|
|
127
|
+
// Close panel
|
|
128
|
+
const panel = document.getElementById("rm-panel");
|
|
129
|
+
if (panel) panel.style.display = "none";
|
|
130
|
+
|
|
131
|
+
// Rerender pins for current page (annotations are global, pins are page-specific)
|
|
132
|
+
this._renderPins();
|
|
133
|
+
this._updateCount();
|
|
134
|
+
this._rebuildList();
|
|
135
|
+
|
|
136
|
+
// Re-init session for new URL
|
|
137
|
+
this._pullNeeded = true;
|
|
138
|
+
if (this.serverOnline) {
|
|
139
|
+
this._synchronizeCurrentPage(true).catch(error => console.warn("[rails-markup] page sync failed:", error));
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
_onTurboFrameRender() {
|
|
143
|
+
// Frame content changed — DOM elements may have moved, reposition pins
|
|
144
|
+
this._repositionPins();
|
|
145
|
+
},
|
|
146
|
+
toggleMode() {
|
|
147
|
+
this.active = !this.active;
|
|
148
|
+
if (this.active) {
|
|
149
|
+
this._activateMode();
|
|
150
|
+
// Always open panel when activating annotation mode
|
|
151
|
+
if (document.getElementById("rm-panel").style.display !== "flex") {
|
|
152
|
+
this.togglePanel();
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
this._deactivateMode();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
_activateMode() {
|
|
159
|
+
document.body.style.cursor = "crosshair";
|
|
160
|
+
const fab = document.getElementById("rm-fab");
|
|
161
|
+
const iconSize = this._fabIconSize();
|
|
162
|
+
fab.style.transform = "scale(0.9)";
|
|
163
|
+
fab.style.boxShadow = `0 0 0 3px ${this._accentBg()}, 0 0 0 6px rgba(99,102,241,0.2)`;
|
|
164
|
+
fab.innerHTML = `<svg viewBox="0 0 24 24" style="width:${iconSize}px;height:${iconSize}px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M6 18L18 6M6 6l12 12"/></svg><span class="rm-fab-badge" id="rm-fab-badge">${document.getElementById("rm-fab-badge")?.textContent || ""}</span>`;
|
|
165
|
+
document.addEventListener("mousemove", this._boundMouseMove, true);
|
|
166
|
+
document.addEventListener("mousedown", this._boundMouseDown, true);
|
|
167
|
+
document.addEventListener("mouseup", this._boundMouseUp, true);
|
|
168
|
+
document.addEventListener("click", this._boundClick, true);
|
|
169
|
+
document.addEventListener("touchstart", this._boundTouchStart, true);
|
|
170
|
+
document.addEventListener("touchend", this._boundTouchEnd, true);
|
|
171
|
+
},
|
|
172
|
+
_deactivateMode() {
|
|
173
|
+
this.active = false;
|
|
174
|
+
document.body.style.cursor = "";
|
|
175
|
+
const fab = document.getElementById("rm-fab");
|
|
176
|
+
if (fab) {
|
|
177
|
+
const iconSize = this._fabIconSize();
|
|
178
|
+
fab.style.transform = "";
|
|
179
|
+
fab.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
|
|
180
|
+
fab.innerHTML = `<svg viewBox="0 0 24 24" style="width:${iconSize}px;height:${iconSize}px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg><span class="rm-fab-badge" id="rm-fab-badge">${this.annotations.length || ""}</span>`;
|
|
181
|
+
this._updateCount();
|
|
182
|
+
}
|
|
183
|
+
document.removeEventListener("mousemove", this._boundMouseMove, true);
|
|
184
|
+
document.removeEventListener("mousedown", this._boundMouseDown, true);
|
|
185
|
+
document.removeEventListener("mouseup", this._boundMouseUp, true);
|
|
186
|
+
document.removeEventListener("click", this._boundClick, true);
|
|
187
|
+
document.removeEventListener("touchstart", this._boundTouchStart, true);
|
|
188
|
+
document.removeEventListener("touchend", this._boundTouchEnd, true);
|
|
189
|
+
this._removeHighlight();
|
|
190
|
+
},
|
|
191
|
+
togglePanel() {
|
|
192
|
+
const panel = document.getElementById("rm-panel");
|
|
193
|
+
const fab = document.getElementById("rm-fab");
|
|
194
|
+
if (panel.style.display === "flex") {
|
|
195
|
+
panel.style.display = "none";
|
|
196
|
+
if (fab) fab.setAttribute("aria-expanded", "false");
|
|
197
|
+
} else {
|
|
198
|
+
panel.style.display = "flex";
|
|
199
|
+
if (fab) fab.setAttribute("aria-expanded", "true");
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
_handleMouseMove(event) {
|
|
203
|
+
if (!this.active) return;
|
|
204
|
+
const el = document.elementFromPoint(event.clientX, event.clientY);
|
|
205
|
+
if (!el || this._isToolbar(el)) { this._removeHighlight(); return; }
|
|
206
|
+
if (el === this.hoveredElement) return;
|
|
207
|
+
this._removeHighlight();
|
|
208
|
+
this.hoveredElement = el;
|
|
209
|
+
el.dataset.rmOrigOutline = el.style.outline || "";
|
|
210
|
+
el.style.outline = `2px solid ${this._accentBg()}`;
|
|
211
|
+
el.style.outlineOffset = "2px";
|
|
212
|
+
},
|
|
213
|
+
_handleMouseDown(event) {
|
|
214
|
+
const el = document.elementFromPoint(event.clientX, event.clientY);
|
|
215
|
+
if (el && !this._isToolbar(el)) {
|
|
216
|
+
this.clickedElement = el;
|
|
217
|
+
// Suppress the press so host controls (buttons, drag handles, form
|
|
218
|
+
// fields) don't act before mouseup/click is blocked. Guard for the
|
|
219
|
+
// synthetic object passed from the touchstart handler.
|
|
220
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
221
|
+
if (typeof event.stopPropagation === "function") event.stopPropagation();
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
async _handleMouseUp(event) {
|
|
225
|
+
if (!this.active) return;
|
|
226
|
+
const el = this.clickedElement || document.elementFromPoint(event.clientX, event.clientY);
|
|
227
|
+
if (!el || this._isToolbar(el)) return;
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
event.stopPropagation();
|
|
230
|
+
const sel = window.getSelection();
|
|
231
|
+
this.selectedText = (sel && sel.toString().trim().length > 0) ? sel.toString().trim() : null;
|
|
232
|
+
this._currentElement = this._identify(el);
|
|
233
|
+
this._currentScreenshot = null;
|
|
234
|
+
if (this.enableScreenshots) {
|
|
235
|
+
this._currentScreenshot = await this._captureElement(el);
|
|
236
|
+
}
|
|
237
|
+
this._showPopup(event.clientX, event.clientY);
|
|
238
|
+
this.clickedElement = null;
|
|
239
|
+
},
|
|
240
|
+
_handleClick(event) {
|
|
241
|
+
if (!this.active) return;
|
|
242
|
+
const el = event.target;
|
|
243
|
+
if (this._isToolbar(el)) return;
|
|
244
|
+
// Block link navigation and Turbo visits while annotating
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
event.stopPropagation();
|
|
247
|
+
},
|
|
248
|
+
_handleKeyDown(event) {
|
|
249
|
+
const openList = this.root?.querySelector(".rm-menu-list.rm-menu-open");
|
|
250
|
+
const openMenu = this._menuForElement(openList);
|
|
251
|
+
if (openMenu) {
|
|
252
|
+
const options = Array.from(openList.querySelectorAll(".rm-menu-option"));
|
|
253
|
+
const focusedIndex = options.indexOf(document.activeElement);
|
|
254
|
+
|
|
255
|
+
if (event.key === "Escape") {
|
|
256
|
+
this._closeMenu(openMenu, true);
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
event.stopPropagation();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (event.key === "Tab") {
|
|
262
|
+
this._closeMenu(openMenu);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
266
|
+
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
267
|
+
const start = focusedIndex === -1 ? 0 : focusedIndex;
|
|
268
|
+
const next = (start + direction + options.length) % options.length;
|
|
269
|
+
options[next]?.focus();
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
event.stopPropagation();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (event.key === "Home" || event.key === "End") {
|
|
275
|
+
const option = event.key === "Home" ? options[0] : options[options.length - 1];
|
|
276
|
+
option?.focus();
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
event.stopPropagation();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if ((event.key === "Enter" || event.key === " ") && focusedIndex !== -1) {
|
|
282
|
+
this._selectMenuOption(options[focusedIndex]);
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
event.stopPropagation();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const trigger = event.target.closest?.(".rm-menu-btn");
|
|
290
|
+
if (trigger && this.root?.contains(trigger) &&
|
|
291
|
+
["Enter", " ", "ArrowDown", "ArrowUp"].includes(event.key)) {
|
|
292
|
+
const menu = this._menuForElement(trigger);
|
|
293
|
+
if (menu) this._openMenu(menu);
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
event.stopPropagation();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (event.key === "Escape") {
|
|
300
|
+
const popup = document.getElementById("rm-popup");
|
|
301
|
+
if (popup && popup.style.display === "block") {
|
|
302
|
+
this._closePopup();
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
event.stopPropagation();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (this.active) {
|
|
308
|
+
this._deactivateMode();
|
|
309
|
+
event.preventDefault();
|
|
310
|
+
event.stopPropagation();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
315
|
+
const popup = document.getElementById("rm-popup");
|
|
316
|
+
if (popup && popup.style.display === "block") {
|
|
317
|
+
this.submitAnnotation();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
_identify(el) {
|
|
322
|
+
const tag = el.tagName.toLowerCase();
|
|
323
|
+
const id = el.id ? "#" + el.id : "";
|
|
324
|
+
const cls = Array.from(el.classList).filter(c => !c.startsWith("rm-")).slice(0, 5).map(c => "." + c).join("");
|
|
325
|
+
const text = (el.textContent || "").trim().slice(0, 80);
|
|
326
|
+
const rect = el.getBoundingClientRect();
|
|
327
|
+
return {
|
|
328
|
+
selector: tag + id + cls,
|
|
329
|
+
cssPath: this._cssPath(el),
|
|
330
|
+
nearbyText: text,
|
|
331
|
+
boundingBox: { top: Math.round(rect.top + window.scrollY), left: Math.round(rect.left + window.scrollX), width: Math.round(rect.width), height: Math.round(rect.height) }
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
_cssPath(el) {
|
|
335
|
+
const parts = [];
|
|
336
|
+
let cur = el;
|
|
337
|
+
while (cur && cur !== document.body && parts.length < 5) {
|
|
338
|
+
let sel = cur.tagName.toLowerCase();
|
|
339
|
+
if (cur.id) { sel += "#" + cur.id; parts.unshift(sel); break; }
|
|
340
|
+
const parent = cur.parentElement;
|
|
341
|
+
if (parent) {
|
|
342
|
+
const sibs = Array.from(parent.children).filter(c => c.tagName === cur.tagName);
|
|
343
|
+
if (sibs.length > 1) sel += ":nth-of-type(" + (sibs.indexOf(cur) + 1) + ")";
|
|
344
|
+
}
|
|
345
|
+
parts.unshift(sel);
|
|
346
|
+
cur = cur.parentElement;
|
|
347
|
+
}
|
|
348
|
+
return parts.join(" > ");
|
|
349
|
+
},
|
|
350
|
+
});
|