Skip to content

Commit a5f7780

Browse files
committed
Suggest updates on a page
1 parent ee6b468 commit a5f7780

File tree

3 files changed

+261
-2
lines changed

3 files changed

+261
-2
lines changed

themes/psh-docs/layouts/_default/baseof.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,16 @@
103103
<!-- copy anchor link if click from the same page -->
104104
<script src="{{ "js/copy-anchor-link.js" | relURL }}"></script>
105105

106-
<!-- copy code -->
106+
<!-- copy code -->
107107
<script src="{{ "js/copy-code.js" | relURL }}"></script>
108108

109+
<!-- GitHub's feedback form -->
110+
<script>
111+
const FEEDBACK_ISSUE_URL = "{{ .Site.Params._404.issues }}";
112+
</script>
113+
<script src="{{ "js/gh-issue-feedback-form.js" | relURL }}"></script>
114+
115+
109116
<!-- allow to zoom images -->
110117
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/medium-zoom.min.js"></script>
111118
<script src="{{ "js/zoom.js" | relURL }}"></script>

themes/psh-docs/layouts/partials/head/head.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<head>
2-
2+
<script>
3+
dataLayer = window.dataLayer || [];
4+
</script>
35
{{ partial "header/gtm_script" . }}
46

57
<!-- Title -->
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
let feedbackButtonTimeout;
2+
3+
document.addEventListener("mouseup", (e) => {
4+
const target = e.target;
5+
6+
// Prevent handling mouseup if clicking on existing button
7+
if (target instanceof Element && target.closest("#feedback-button")) return;
8+
9+
// Ignore clicks inside the feedback modal
10+
if (target instanceof Element && target.closest("#feedback-overlay")) return;
11+
12+
const selection = window.getSelection();
13+
const parent = selection.anchorNode.parentElement;
14+
15+
16+
// Check if selection is inside <main>
17+
const main = parent.closest("main");
18+
if (!main) return; // not in main, ignore
19+
20+
// Check if selection is in <h1> OR in a <div class="prose">
21+
const allowed = parent.closest("main h1, main div.prose");
22+
23+
24+
if (!allowed) return; // not in allowed area, ignore
25+
26+
const text = selection.toString().trim();
27+
if (!text || text.length < 5) return;
28+
29+
// Remove all existing buttons and popups immediately
30+
document.querySelectorAll("#feedback-button, #feedback-overlay").forEach(el => el.remove());
31+
32+
// Clear any previous timeout
33+
clearTimeout(feedbackButtonTimeout);
34+
35+
// Remove older button
36+
setTimeout(() => {
37+
const oldButton = document.getElementById("feedback-button");
38+
if (oldButton) oldButton.remove();
39+
}, 2000);
40+
41+
// Delay creation slightly to avoid double-click flash
42+
feedbackMouseupTimeout = setTimeout(() => {
43+
// Remove existing buttons/popups
44+
45+
const selection = window.getSelection();
46+
const range = selection.getRangeAt(0);
47+
const rect = range.getBoundingClientRect();
48+
49+
// Create floating button
50+
const button = document.createElement("button");
51+
button.id = "feedback-button";
52+
button.textContent = "💬 Suggest edit";
53+
button.style.position = "fixed";
54+
button.style.top = `${rect.bottom + 8}px`;
55+
button.style.left = `${rect.left }px`;
56+
button.style.zIndex = 9999;
57+
button.style.padding = "4px 8px";
58+
button.style.borderRadius = "6px";
59+
button.style.background = "#2563eb";
60+
button.style.color = "white";
61+
button.style.border = "none";
62+
button.style.cursor = "pointer";
63+
button.style.fontSize = "0.85rem";
64+
button.style.opacity = "0";
65+
button.style.transition = "opacity 0.3s ease";
66+
67+
document.body.appendChild(button);
68+
69+
// Fade-in
70+
requestAnimationFrame(() => button.style.opacity = "1");
71+
72+
// Function to remove button
73+
const hideButton = () => {
74+
button.style.opacity = "0";
75+
setTimeout(() => button.remove(), 600);
76+
};
77+
78+
// Auto-hide after 2.5s
79+
feedbackButtonTimeout = setTimeout(hideButton, 2500);
80+
81+
// Keep button visible while hovering
82+
button.addEventListener("mouseenter", () => clearTimeout(feedbackButtonTimeout));
83+
button.addEventListener("mouseleave", () => {
84+
feedbackButtonTimeout = setTimeout(hideButton, 1500); // short delay after leaving
85+
});
86+
87+
button.addEventListener("click", () => {
88+
// Clear the auto-hide to prevent the button or modal from disappearing
89+
clearTimeout(feedbackButtonTimeout);
90+
91+
// Remove button (optional)
92+
button.remove();
93+
94+
// Show modal below the selected text, passing the selection
95+
showFeedbackModalBelow(text, window.getSelection());
96+
});
97+
}, 50); // 50ms debounce to prevent double-click flash
98+
99+
});
100+
101+
// Display centered feedback modal
102+
function showFeedbackModalBelow(selectedText, selection) {
103+
const overlay = document.createElement("div");
104+
overlay.id = "feedback-overlay";
105+
overlay.style.position = "fixed";
106+
overlay.style.top = 0;
107+
overlay.style.left = 0;
108+
overlay.style.width = "100%";
109+
overlay.style.height = "100%";
110+
overlay.style.background = "rgba(0, 0, 0, 0.4)";
111+
overlay.style.display = "flex";
112+
overlay.style.alignItems = "center";
113+
overlay.style.justifyContent = "center";
114+
overlay.style.zIndex = 10000;
115+
116+
const mainBlock = document.querySelector("main"); // adjust selector to your content block
117+
const mainWidth = mainBlock ? mainBlock.offsetWidth : "501px"; // fallback width
118+
119+
const modal = document.createElement("div");
120+
modal.style.background = "white";
121+
modal.style.padding = "1.5rem";
122+
modal.style.borderRadius = "10px";
123+
modal.style.maxWidth = mainWidth;
124+
modal.style.setProperty("max-width", mainWidth + "px", "important");
125+
modal.style.width = "80%";
126+
modal.style.boxShadow = "0 5px 20px rgba(0,0,0,0.3)";
127+
modal.className = "prose"; // keep Tailwind styles
128+
129+
// Clone the selection
130+
let htmlFragment = "";
131+
if (selection && selection.rangeCount > 0) {
132+
const frag = selection.getRangeAt(0).cloneContents();
133+
const div = document.createElement("div");
134+
div.appendChild(frag);
135+
136+
// Copy computed styles from each element recursively
137+
function getStyledSelectionHTML(selection) {
138+
if (!selection || selection.rangeCount === 0) return "";
139+
140+
const frag = selection.getRangeAt(0).cloneContents();
141+
const container = document.createElement("div");
142+
143+
// Special case for title
144+
const parent = selection.anchorNode?.parentElement;
145+
if (parent && /^H[1-6]$/.test(parent.tagName)) {
146+
const h1Clone = parent.cloneNode(true); // clone juste le H1
147+
container.appendChild(h1Clone);
148+
149+
// Copy computed style
150+
const computed = window.getComputedStyle(parent);
151+
h1Clone.style.fontFamily = computed.fontFamily;
152+
h1Clone.style.fontSize = computed.fontSize;
153+
h1Clone.style.fontWeight = computed.fontWeight;
154+
h1Clone.style.color = computed.color;
155+
h1Clone.style.lineHeight = computed.lineHeight;
156+
157+
return container.innerHTML;
158+
}
159+
160+
container.appendChild(frag);
161+
162+
// Copy basic styles from parent element
163+
if (parent) {
164+
const computed = window.getComputedStyle(parent);
165+
container.style.fontFamily = computed.fontFamily;
166+
container.style.fontSize = computed.fontSize;
167+
container.style.fontWeight = computed.fontWeight;
168+
container.style.color = computed.color;
169+
container.style.lineHeight = computed.lineHeight;
170+
}
171+
172+
return container.innerHTML;
173+
}
174+
htmlFragment = getStyledSelectionHTML(selection);
175+
}
176+
177+
modal.innerHTML = `
178+
<h3 style="margin-top:0;">💬 Suggest an edit</h3>
179+
180+
<p style="font-size:0.9rem; color:#444;">Selected text (styled as in page):</p>
181+
<div style="background:#f9fafb; padding:0.5rem 0.75rem; border-left:3px solid #2563eb; margin:0 0 1rem;">
182+
${htmlFragment}
183+
</div>
184+
185+
<p style="font-size:0.9rem; color:#444;">Your suggestion:</p>
186+
<textarea id="feedback-text" placeholder="Your suggestion..." style="width:100%; height:100px; padding:0.5rem; border:1px solid #ccc; border-radius:6px; resize:vertical;">
187+
${selection.toString().trim()}
188+
</textarea>
189+
190+
<div style="margin-top:1rem; display:flex; justify-content:flex-end; gap:0.5rem;">
191+
<button id="cancel-feedback" style="background:#e5e7eb; border:none; padding:0.5rem 1rem; border-radius:6px; cursor:pointer;">Cancel</button>
192+
<button id="submit-feedback" style="background:#2563eb; color:white; border:none; padding:0.5rem 1rem; border-radius:6px; cursor:pointer;">Submit</button>
193+
</div>
194+
`;
195+
196+
overlay.appendChild(modal);
197+
document.body.appendChild(overlay);
198+
199+
const feedbackInput = modal.querySelector("#feedback-text");
200+
feedbackInput.focus();
201+
202+
// Close modal on cancel
203+
document.getElementById("cancel-feedback").onclick = () => overlay.remove();
204+
205+
// Submit feedback (opens GitHub issue)
206+
document.getElementById("submit-feedback").onclick = () => {
207+
const feedback = feedbackInput.value.trim();
208+
if (!feedback) return alert("Please enter your suggestion!");
209+
210+
// Use the selection only for anchor, fallback if lost
211+
const selection = window.getSelection();
212+
const anchor = getClosestAnchor(selection.anchorNode.parentElement);
213+
const url = window.location.href.split('#')[0] + anchor;
214+
215+
const title = encodeURIComponent("Content feedback: " + url);
216+
const issueBaseUrl = typeof FEEDBACK_ISSUE_URL !== "undefined" ? FEEDBACK_ISSUE_URL : "https://github.com/platformsh/platformsh-docs/issues/new";
217+
const body = encodeURIComponent(
218+
`### 📝 Selected text
219+
\`\`\`
220+
${selectedText}
221+
\`\`\`
222+
223+
### 💡 Suggestion
224+
${feedback}
225+
226+
### 🌐 Page
227+
${url}`
228+
);
229+
230+
const issueUrl = `${issueBaseUrl}?title=${title}&body=${body}`;
231+
window.open(issueUrl, "_blank");
232+
overlay.remove();
233+
};
234+
235+
// Close modal on ESC key
236+
document.addEventListener("keydown", (e) => {
237+
if (e.key === "Escape") overlay.remove();
238+
});
239+
}
240+
241+
function getClosestAnchor(element) {
242+
let current = element;
243+
while (current) {
244+
if (current.id) {
245+
return `#${current.id}`; // return fragment
246+
}
247+
current = current.parentElement;
248+
}
249+
return ""; // no anchor found
250+
}

0 commit comments

Comments
 (0)