@consilioweb/payload-support 0.9.8 → 0.9.10

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.
@@ -99,6 +99,43 @@ const RichTextEditor = react.forwardRef(function RichTextEditor2({
99
99
  }
100
100
  if (fileInputRef.current) fileInputRef.current.value = "";
101
101
  }, [onFileUpload, emitChange]);
102
+ const handleClearFormat = react.useCallback(() => {
103
+ document.execCommand("removeFormat");
104
+ document.execCommand("formatBlock", false, "p");
105
+ editorRef.current?.focus();
106
+ setTimeout(emitChange, 0);
107
+ }, [emitChange]);
108
+ const handleKeyDown = react.useCallback((e) => {
109
+ if (e.key !== "Enter" || e.shiftKey) return;
110
+ const sel = window.getSelection();
111
+ if (!sel || sel.rangeCount === 0) return;
112
+ let node = sel.anchorNode;
113
+ let blockquote = null;
114
+ while (node && node !== editorRef.current) {
115
+ if (node.nodeName === "BLOCKQUOTE") {
116
+ blockquote = node;
117
+ break;
118
+ }
119
+ node = node.parentNode;
120
+ }
121
+ if (!blockquote) return;
122
+ const text = blockquote.innerText || "";
123
+ if (text.replace(/\n+$/, "").length === 0 || text.endsWith("\n\n") || text.endsWith("\n")) {
124
+ e.preventDefault();
125
+ const p = document.createElement("p");
126
+ p.innerHTML = "<br>";
127
+ blockquote.after(p);
128
+ if (blockquote.lastChild && blockquote.lastChild.nodeName === "BR") {
129
+ blockquote.removeChild(blockquote.lastChild);
130
+ }
131
+ const range = document.createRange();
132
+ range.setStart(p, 0);
133
+ range.collapse(true);
134
+ sel.removeAllRanges();
135
+ sel.addRange(range);
136
+ setTimeout(emitChange, 0);
137
+ }
138
+ }, [emitChange]);
102
139
  const handlePaste = react.useCallback(async (e) => {
103
140
  if (!onFileUpload) return;
104
141
  const items = e.clipboardData?.items;
@@ -186,7 +223,15 @@ const RichTextEditor = react.forwardRef(function RichTextEditor2({
186
223
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: btn, onMouseDown: (e) => {
187
224
  e.preventDefault();
188
225
  handleImageClick();
189
- }, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" })
226
+ }, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" }),
227
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: sep }),
228
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", style: { ...btn, fontSize: "13px" }, onMouseDown: (e) => {
229
+ e.preventDefault();
230
+ handleClearFormat();
231
+ }, title: "Effacer la mise en forme", children: [
232
+ "T",
233
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "10px", verticalAlign: "super" }, children: "\xD7" })
234
+ ] })
190
235
  ] }),
191
236
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
192
237
  isEmpty && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", top: 0, left: 0, right: 0, padding: "14px 16px", color: "#9ca3af", fontSize: "14px", pointerEvents: "none", userSelect: "none" }, children: placeholder }),
@@ -204,6 +249,7 @@ const RichTextEditor = react.forwardRef(function RichTextEditor2({
204
249
  emitChange();
205
250
  },
206
251
  onPaste: handlePaste,
252
+ onKeyDown: handleKeyDown,
207
253
  style: {
208
254
  minHeight: `${minHeight}px`,
209
255
  padding: "14px 16px",
@@ -98,6 +98,43 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
98
98
  }
99
99
  if (fileInputRef.current) fileInputRef.current.value = "";
100
100
  }, [onFileUpload, emitChange]);
101
+ const handleClearFormat = useCallback(() => {
102
+ document.execCommand("removeFormat");
103
+ document.execCommand("formatBlock", false, "p");
104
+ editorRef.current?.focus();
105
+ setTimeout(emitChange, 0);
106
+ }, [emitChange]);
107
+ const handleKeyDown = useCallback((e) => {
108
+ if (e.key !== "Enter" || e.shiftKey) return;
109
+ const sel = window.getSelection();
110
+ if (!sel || sel.rangeCount === 0) return;
111
+ let node = sel.anchorNode;
112
+ let blockquote = null;
113
+ while (node && node !== editorRef.current) {
114
+ if (node.nodeName === "BLOCKQUOTE") {
115
+ blockquote = node;
116
+ break;
117
+ }
118
+ node = node.parentNode;
119
+ }
120
+ if (!blockquote) return;
121
+ const text = blockquote.innerText || "";
122
+ if (text.replace(/\n+$/, "").length === 0 || text.endsWith("\n\n") || text.endsWith("\n")) {
123
+ e.preventDefault();
124
+ const p = document.createElement("p");
125
+ p.innerHTML = "<br>";
126
+ blockquote.after(p);
127
+ if (blockquote.lastChild && blockquote.lastChild.nodeName === "BR") {
128
+ blockquote.removeChild(blockquote.lastChild);
129
+ }
130
+ const range = document.createRange();
131
+ range.setStart(p, 0);
132
+ range.collapse(true);
133
+ sel.removeAllRanges();
134
+ sel.addRange(range);
135
+ setTimeout(emitChange, 0);
136
+ }
137
+ }, [emitChange]);
101
138
  const handlePaste = useCallback(async (e) => {
102
139
  if (!onFileUpload) return;
103
140
  const items = e.clipboardData?.items;
@@ -185,7 +222,15 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
185
222
  /* @__PURE__ */ jsx("button", { type: "button", style: btn, onMouseDown: (e) => {
186
223
  e.preventDefault();
187
224
  handleImageClick();
188
- }, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" })
225
+ }, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" }),
226
+ /* @__PURE__ */ jsx("span", { style: sep }),
227
+ /* @__PURE__ */ jsxs("button", { type: "button", style: { ...btn, fontSize: "13px" }, onMouseDown: (e) => {
228
+ e.preventDefault();
229
+ handleClearFormat();
230
+ }, title: "Effacer la mise en forme", children: [
231
+ "T",
232
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "10px", verticalAlign: "super" }, children: "\xD7" })
233
+ ] })
189
234
  ] }),
190
235
  /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
191
236
  isEmpty && /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: 0, left: 0, right: 0, padding: "14px 16px", color: "#9ca3af", fontSize: "14px", pointerEvents: "none", userSelect: "none" }, children: placeholder }),
@@ -203,6 +248,7 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
203
248
  emitChange();
204
249
  },
205
250
  onPaste: handlePaste,
251
+ onKeyDown: handleKeyDown,
206
252
  style: {
207
253
  minHeight: `${minHeight}px`,
208
254
  padding: "14px 16px",
@@ -832,10 +832,32 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
832
832
  cannedResponses.map((cr) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
833
833
  ] })
834
834
  ] }),
835
- /* @__PURE__ */ jsxRuntime.jsx(index.RichTextEditor, { ref: editorRef, onChange: (html, text) => {
836
- setReplyHtml(html);
837
- setReplyBody(text);
838
- }, placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }), minHeight: 100, borderColor: "transparent" }),
835
+ /* @__PURE__ */ jsxRuntime.jsx(
836
+ index.RichTextEditor,
837
+ {
838
+ ref: editorRef,
839
+ onChange: (html, text) => {
840
+ setReplyHtml(html);
841
+ setReplyBody(text);
842
+ },
843
+ placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }),
844
+ minHeight: 100,
845
+ borderColor: "transparent",
846
+ onFileUpload: async (file) => {
847
+ try {
848
+ const formData = new FormData();
849
+ formData.append("file", file);
850
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
851
+ const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
852
+ if (!ur.ok) return null;
853
+ const ud = await ur.json();
854
+ return ud.doc?.url || null;
855
+ } catch {
856
+ return null;
857
+ }
858
+ }
859
+ }
860
+ ),
839
861
  pendingFiles.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.uploadPreviewItem, children: [
840
862
  /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
841
863
  "PJ ",
@@ -825,10 +825,32 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
825
825
  cannedResponses.map((cr) => /* @__PURE__ */ jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
826
826
  ] })
827
827
  ] }),
828
- /* @__PURE__ */ jsx(RichTextEditor, { ref: editorRef, onChange: (html, text) => {
829
- setReplyHtml(html);
830
- setReplyBody(text);
831
- }, placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }), minHeight: 100, borderColor: "transparent" }),
828
+ /* @__PURE__ */ jsx(
829
+ RichTextEditor,
830
+ {
831
+ ref: editorRef,
832
+ onChange: (html, text) => {
833
+ setReplyHtml(html);
834
+ setReplyBody(text);
835
+ },
836
+ placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }),
837
+ minHeight: 100,
838
+ borderColor: "transparent",
839
+ onFileUpload: async (file) => {
840
+ try {
841
+ const formData = new FormData();
842
+ formData.append("file", file);
843
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
844
+ const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
845
+ if (!ur.ok) return null;
846
+ const ud = await ur.json();
847
+ return ud.doc?.url || null;
848
+ } catch {
849
+ return null;
850
+ }
851
+ }
852
+ }
853
+ ),
832
854
  pendingFiles.length > 0 && /* @__PURE__ */ jsx("div", { className: s.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxs("div", { className: s.uploadPreviewItem, children: [
833
855
  /* @__PURE__ */ jsxs("span", { children: [
834
856
  "PJ ",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -129,6 +129,49 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
129
129
  if (fileInputRef.current) fileInputRef.current.value = ''
130
130
  }, [onFileUpload, emitChange])
131
131
 
132
+ const handleClearFormat = useCallback(() => {
133
+ document.execCommand('removeFormat')
134
+ document.execCommand('formatBlock', false, 'p')
135
+ editorRef.current?.focus()
136
+ setTimeout(emitChange, 0)
137
+ }, [emitChange])
138
+
139
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
140
+ if (e.key !== 'Enter' || e.shiftKey) return
141
+ const sel = window.getSelection()
142
+ if (!sel || sel.rangeCount === 0) return
143
+ // Find closest blockquote ancestor of the caret
144
+ let node: Node | null = sel.anchorNode
145
+ let blockquote: HTMLElement | null = null
146
+ while (node && node !== editorRef.current) {
147
+ if ((node as HTMLElement).nodeName === 'BLOCKQUOTE') {
148
+ blockquote = node as HTMLElement
149
+ break
150
+ }
151
+ node = node.parentNode
152
+ }
153
+ if (!blockquote) return
154
+ // Exit on Enter when the last line of the blockquote is empty
155
+ const text = blockquote.innerText || ''
156
+ if (text.replace(/\n+$/, '').length === 0 || text.endsWith('\n\n') || text.endsWith('\n')) {
157
+ e.preventDefault()
158
+ // Insert a paragraph after the blockquote and move caret into it
159
+ const p = document.createElement('p')
160
+ p.innerHTML = '<br>'
161
+ blockquote.after(p)
162
+ // Clean trailing empty <br> inside blockquote
163
+ if (blockquote.lastChild && (blockquote.lastChild as HTMLElement).nodeName === 'BR') {
164
+ blockquote.removeChild(blockquote.lastChild)
165
+ }
166
+ const range = document.createRange()
167
+ range.setStart(p, 0)
168
+ range.collapse(true)
169
+ sel.removeAllRanges()
170
+ sel.addRange(range)
171
+ setTimeout(emitChange, 0)
172
+ }
173
+ }, [emitChange])
174
+
132
175
  const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
133
176
  if (!onFileUpload) return
134
177
  const items = e.clipboardData?.items
@@ -215,6 +258,10 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
215
258
  <button type="button" style={btn} onMouseDown={(e) => { e.preventDefault(); handleImageClick() }} title="Insérer une image">
216
259
  &#128444;&#65039;
217
260
  </button>
261
+ <span style={sep} />
262
+ <button type="button" style={{ ...btn, fontSize: '13px' }} onMouseDown={(e) => { e.preventDefault(); handleClearFormat() }} title="Effacer la mise en forme">
263
+ T<span style={{ fontSize: '10px', verticalAlign: 'super' }}>&times;</span>
264
+ </button>
218
265
  </div>
219
266
 
220
267
  {/* Editor area */}
@@ -233,6 +280,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
233
280
  onFocus={() => setFocused(true)}
234
281
  onBlur={() => { setFocused(false); emitChange() }}
235
282
  onPaste={handlePaste}
283
+ onKeyDown={handleKeyDown}
236
284
  style={{
237
285
  minHeight: `${minHeight}px`,
238
286
  padding: '14px 16px',
@@ -764,7 +764,24 @@ const [clientTyping, setClientTyping] = useState(false)
764
764
  </select>
765
765
  )}
766
766
  </div>
767
- <RichTextEditor ref={editorRef} onChange={(html, text) => { setReplyHtml(html); setReplyBody(text) }} placeholder={isInternal ? t('composer.placeholderInternal') : t('composer.placeholderReplyTo', { name: client?.firstName || 'client' })} minHeight={100} borderColor="transparent" />
767
+ <RichTextEditor
768
+ ref={editorRef}
769
+ onChange={(html, text) => { setReplyHtml(html); setReplyBody(text) }}
770
+ placeholder={isInternal ? t('composer.placeholderInternal') : t('composer.placeholderReplyTo', { name: client?.firstName || 'client' })}
771
+ minHeight={100}
772
+ borderColor="transparent"
773
+ onFileUpload={async (file) => {
774
+ try {
775
+ const formData = new FormData()
776
+ formData.append('file', file)
777
+ formData.append('_payload', JSON.stringify({ alt: file.name }))
778
+ const ur = await fetch('/api/media', { method: 'POST', credentials: 'include', body: formData })
779
+ if (!ur.ok) return null
780
+ const ud = await ur.json()
781
+ return ud.doc?.url || null
782
+ } catch { return null }
783
+ }}
784
+ />
768
785
  {/* #5 — File upload preview */}
769
786
  {pendingFiles.length > 0 && (
770
787
  <div className={s.uploadPreview}>