@skalfa/skalfa-component 1.0.34 → 1.0.36

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.
@@ -458,6 +458,68 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
458
458
  }
459
459
  }
460
460
  }, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink, handleEditorChange, updateActiveStates]);
461
+ const handlePaste = (0, react_1.useCallback)((e) => {
462
+ const pastedText = e.clipboardData.getData("text/plain");
463
+ if (!pastedText)
464
+ return;
465
+ const urlRegex = /(\b(?:https?:\/\/|www\.)[^\s()<>]+(?:\([\w\d]+\)|[^.,?;:"')\s]))/gi;
466
+ if (urlRegex.test(pastedText)) {
467
+ e.preventDefault();
468
+ const escapeHtml = (text) => {
469
+ return text
470
+ .replace(/&/g, "&amp;")
471
+ .replace(/</g, "&lt;")
472
+ .replace(/>/g, "&gt;")
473
+ .replace(/"/g, "&quot;")
474
+ .replace(/'/g, "&#039;");
475
+ };
476
+ const tokens = [];
477
+ let lastIndex = 0;
478
+ let match;
479
+ urlRegex.lastIndex = 0;
480
+ while ((match = urlRegex.exec(pastedText)) !== null) {
481
+ if (match.index > lastIndex) {
482
+ tokens.push(escapeHtml(pastedText.substring(lastIndex, match.index)));
483
+ }
484
+ const url = match[0];
485
+ let href = url;
486
+ if (url.toLowerCase().startsWith("www.")) {
487
+ href = "https://" + url;
488
+ }
489
+ const escapedHref = escapeHtml(href);
490
+ const escapedUrl = escapeHtml(url);
491
+ tokens.push(`<a href="${escapedHref}" class="text-primary underline" data-link="${escapedHref}" target="_blank" rel="noopener">${escapedUrl}</a>`);
492
+ lastIndex = urlRegex.lastIndex;
493
+ }
494
+ if (lastIndex < pastedText.length) {
495
+ tokens.push(escapeHtml(pastedText.substring(lastIndex)));
496
+ }
497
+ const htmlContent = tokens.join("").replace(/\n/g, "<br>");
498
+ const sel = window.getSelection();
499
+ if (!sel || sel.rangeCount === 0)
500
+ return;
501
+ const range = sel.getRangeAt(0);
502
+ range.deleteContents();
503
+ const tempDiv = document.createElement("div");
504
+ tempDiv.innerHTML = htmlContent;
505
+ const fragment = document.createDocumentFragment();
506
+ let child;
507
+ while ((child = tempDiv.firstChild)) {
508
+ fragment.appendChild(child);
509
+ }
510
+ const lastNode = fragment.lastChild;
511
+ range.insertNode(fragment);
512
+ if (lastNode) {
513
+ const newRange = document.createRange();
514
+ newRange.setStartAfter(lastNode);
515
+ newRange.collapse(true);
516
+ sel.removeAllRanges();
517
+ sel.addRange(newRange);
518
+ }
519
+ saveSelection();
520
+ handleEditorChange();
521
+ }
522
+ }, [saveSelection, handleEditorChange]);
461
523
  const renderControlItem = (0, react_1.useCallback)((item, index) => {
462
524
  if (typeof item !== "string") {
463
525
  return (0, jsx_runtime_1.jsx)("div", { children: item }, index);
@@ -555,7 +617,7 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
555
617
  saveSelection();
556
618
  handleEditorChange();
557
619
  updateActiveStates();
558
- }, onKeyDown: handleKeyDown, onMouseUp: () => {
620
+ }, onKeyDown: handleKeyDown, onPaste: handlePaste, onMouseUp: () => {
559
621
  saveSelection();
560
622
  updateActiveStates();
561
623
  }, onKeyUp: () => {
@@ -9,7 +9,7 @@ const _utils_1 = require("@utils");
9
9
  const BottomSheet_component_1 = require("../modal/BottomSheet.component");
10
10
  const Button_component_1 = require("../button/Button.component");
11
11
  const Modal_component_1 = require("../modal/Modal.component");
12
- const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1", invalid, validations, onChange, register, unregister, className = "", }) => {
12
+ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invalid, validations, onChange, register, unregister, className = "", }) => {
13
13
  const { isSm } = (0, _utils_1.useResponsive)();
14
14
  const randomId = (0, _utils_1.useInputRandomId)();
15
15
  const inputRef = (0, react_1.useRef)(null);
@@ -19,6 +19,9 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1"
19
19
  const [openCrop, setOpenCrop] = (0, react_1.useState)(false);
20
20
  const inputHandler = (0, _utils_1.useInputHandler)(name, value, validations, register, true, unregister);
21
21
  const [invalidMessage, setInvalidMessage] = (0, _utils_1.useValidation)(inputHandler.value, validations, invalid, inputHandler.idle);
22
+ const isCroppingEnabled = !!aspect;
23
+ const currentAspect = aspect || "1/1";
24
+ const containerAspect = (preview && !isCroppingEnabled) ? undefined : currentAspect;
22
25
  (0, react_1.useEffect)(() => {
23
26
  if (value) {
24
27
  const url = typeof value === "object" ? URL.createObjectURL(value) : value;
@@ -48,7 +51,14 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1"
48
51
  setInvalidMessage("Format gambar tidak diperbolehkan (JPG/PNG/WebP)");
49
52
  return;
50
53
  }
51
- openCropper(file);
54
+ if (isCroppingEnabled) {
55
+ openCropper(file);
56
+ }
57
+ else {
58
+ const url = URL.createObjectURL(file);
59
+ setPreview(url);
60
+ onChange?.(file);
61
+ }
52
62
  };
53
63
  const onCropDone = (file) => {
54
64
  const url = URL.createObjectURL(file);
@@ -69,17 +79,27 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1"
69
79
  e.preventDefault();
70
80
  setDrag(false);
71
81
  const file = e.dataTransfer.files?.[0];
72
- file && openCropper(file);
82
+ if (!file)
83
+ return;
84
+ if (isCroppingEnabled) {
85
+ openCropper(file);
86
+ }
87
+ else {
88
+ const url = URL.createObjectURL(file);
89
+ setPreview(url);
90
+ onChange?.(file);
91
+ }
73
92
  };
74
93
  return ((0, jsx_runtime_1.jsxs)("div", { className: "input-container w-full", children: [label && ((0, jsx_runtime_1.jsxs)("label", { htmlFor: randomId, className: (0, _utils_1.cn)("input-label", disabled && "input-label-disabled", inputHandler.focus && "input-label-focus", !!invalidMessage && "input-label-error", (0, _utils_1.pcn)(className, "label")), children: [label, validations && _utils_1.validation.hasRules(validations, "required") && (0, jsx_runtime_1.jsx)("span", { className: "text-danger ml-1", children: "*" })] })), tip && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-tip", disabled && "input-tip-disabled", (0, _utils_1.pcn)(className, "tip")), children: tip })), (0, jsx_runtime_1.jsx)("label", { htmlFor: randomId, children: (0, jsx_runtime_1.jsxs)("div", { className: (0, _utils_1.cn)("input-image-dropzone", drag ? "input-image-dropzone-drag" : "", invalidMessage && "input-image-dropzone-error", (0, _utils_1.pcn)(className, "input")), style: {
75
- aspectRatio: aspect,
76
- backgroundImage: preview ? `url(${preview})` : undefined,
94
+ position: "relative",
95
+ aspectRatio: containerAspect,
96
+ backgroundImage: preview && containerAspect ? `url(${preview})` : undefined,
77
97
  backgroundSize: "cover",
78
98
  backgroundPosition: "center",
79
99
  }, onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => {
80
100
  e.preventDefault();
81
101
  setDrag(true);
82
- }, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] }), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-max max-w-[350px]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(aspect.replace(":", "/")), onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(aspect.replace(":", "/")), onDone: onCropDone }) })) }))] }));
102
+ }, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-max max-w-[350px]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) }))] }));
83
103
  };
84
104
  exports.InputImageComponent = InputImageComponent;
85
105
  const CanvasCropper = ({ src, aspect, onDone, }) => {
@@ -58,7 +58,7 @@ export type TableSupervisionProps = {
58
58
  description: string;
59
59
  };
60
60
  on?: (item: Record<string, any>) => boolean;
61
- } | ((row: object, setModal?: (type: "EDIT" | "DELETE") => void, setDataSelected?: () => void, reset?: () => void, size?: string) => ReactNode))[];
61
+ } | ((row: Record<string, any>, setModal?: (type: "EDIT" | "DELETE") => void, setDataSelected?: () => void, reset?: () => void, size?: string) => ReactNode))[];
62
62
  block?: boolean;
63
63
  noIndex?: boolean;
64
64
  actionBulkingControl?: TableProps["actionBulking"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-component",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Reusable UI components for Skalfa App.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -574,6 +574,83 @@ export function InputContentComponent({
574
574
  }
575
575
  }, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink, handleEditorChange, updateActiveStates]);
576
576
 
577
+ const handlePaste = useCallback((e: React.ClipboardEvent<HTMLDivElement>) => {
578
+ const pastedText = e.clipboardData.getData("text/plain");
579
+ if (!pastedText) return;
580
+
581
+ const urlRegex = /(\b(?:https?:\/\/|www\.)[^\s()<>]+(?:\([\w\d]+\)|[^.,?;:"')\s]))/gi;
582
+
583
+ if (urlRegex.test(pastedText)) {
584
+ e.preventDefault();
585
+
586
+ const escapeHtml = (text: string) => {
587
+ return text
588
+ .replace(/&/g, "&amp;")
589
+ .replace(/</g, "&lt;")
590
+ .replace(/>/g, "&gt;")
591
+ .replace(/"/g, "&quot;")
592
+ .replace(/'/g, "&#039;");
593
+ };
594
+
595
+ const tokens: string[] = [];
596
+ let lastIndex = 0;
597
+ let match;
598
+ urlRegex.lastIndex = 0;
599
+
600
+ while ((match = urlRegex.exec(pastedText)) !== null) {
601
+ if (match.index > lastIndex) {
602
+ tokens.push(escapeHtml(pastedText.substring(lastIndex, match.index)));
603
+ }
604
+
605
+ const url = match[0];
606
+ let href = url;
607
+ if (url.toLowerCase().startsWith("www.")) {
608
+ href = "https://" + url;
609
+ }
610
+
611
+ const escapedHref = escapeHtml(href);
612
+ const escapedUrl = escapeHtml(url);
613
+ tokens.push(`<a href="${escapedHref}" class="text-primary underline" data-link="${escapedHref}" target="_blank" rel="noopener">${escapedUrl}</a>`);
614
+
615
+ lastIndex = urlRegex.lastIndex;
616
+ }
617
+
618
+ if (lastIndex < pastedText.length) {
619
+ tokens.push(escapeHtml(pastedText.substring(lastIndex)));
620
+ }
621
+
622
+ const htmlContent = tokens.join("").replace(/\n/g, "<br>");
623
+
624
+ const sel = window.getSelection();
625
+ if (!sel || sel.rangeCount === 0) return;
626
+ const range = sel.getRangeAt(0);
627
+ range.deleteContents();
628
+
629
+ const tempDiv = document.createElement("div");
630
+ tempDiv.innerHTML = htmlContent;
631
+
632
+ const fragment = document.createDocumentFragment();
633
+ let child: Node | null;
634
+ while ((child = tempDiv.firstChild)) {
635
+ fragment.appendChild(child);
636
+ }
637
+
638
+ const lastNode = fragment.lastChild;
639
+ range.insertNode(fragment);
640
+
641
+ if (lastNode) {
642
+ const newRange = document.createRange();
643
+ newRange.setStartAfter(lastNode);
644
+ newRange.collapse(true);
645
+ sel.removeAllRanges();
646
+ sel.addRange(newRange);
647
+ }
648
+
649
+ saveSelection();
650
+ handleEditorChange();
651
+ }
652
+ }, [saveSelection, handleEditorChange]);
653
+
577
654
  const renderControlItem = useCallback((item: ToolbarControlItem, index: number) => {
578
655
  if (typeof item !== "string") {
579
656
  return <div key={index}>{item}</div>;
@@ -945,6 +1022,7 @@ export function InputContentComponent({
945
1022
  updateActiveStates();
946
1023
  }}
947
1024
  onKeyDown={handleKeyDown}
1025
+ onPaste={handlePaste}
948
1026
  onMouseUp={() => {
949
1027
  saveSelection();
950
1028
  updateActiveStates();
@@ -37,7 +37,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
37
37
 
38
38
  value,
39
39
  disabled,
40
- aspect = "1/1",
40
+ aspect,
41
41
  invalid,
42
42
  validations,
43
43
 
@@ -59,6 +59,10 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
59
59
  const inputHandler = useInputHandler(name, value, validations, register, true, unregister);
60
60
  const [invalidMessage, setInvalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
61
61
 
62
+ const isCroppingEnabled = !!aspect;
63
+ const currentAspect = aspect || "1/1";
64
+ const containerAspect = (preview && !isCroppingEnabled) ? undefined : currentAspect;
65
+
62
66
  useEffect(() => {
63
67
  if (value) {
64
68
  const url = typeof value === "object" ? URL.createObjectURL(value) : value;
@@ -91,7 +95,13 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
91
95
  return;
92
96
  }
93
97
 
94
- openCropper(file);
98
+ if (isCroppingEnabled) {
99
+ openCropper(file);
100
+ } else {
101
+ const url = URL.createObjectURL(file);
102
+ setPreview(url);
103
+ onChange?.(file);
104
+ }
95
105
  };
96
106
 
97
107
  const onCropDone = (file: File) => {
@@ -117,7 +127,15 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
117
127
  setDrag(false);
118
128
 
119
129
  const file = e.dataTransfer.files?.[0];
120
- file && openCropper(file);
130
+ if (!file) return;
131
+
132
+ if (isCroppingEnabled) {
133
+ openCropper(file);
134
+ } else {
135
+ const url = URL.createObjectURL(file);
136
+ setPreview(url);
137
+ onChange?.(file);
138
+ }
121
139
  };
122
140
 
123
141
  return (
@@ -157,8 +175,9 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
157
175
  pcn<CT>(className, "input")
158
176
  )}
159
177
  style={{
160
- aspectRatio: aspect,
161
- backgroundImage: preview ? `url(${preview})` : undefined,
178
+ position: "relative",
179
+ aspectRatio: containerAspect,
180
+ backgroundImage: preview && containerAspect ? `url(${preview})` : undefined,
162
181
  backgroundSize: "cover",
163
182
  backgroundPosition: "center",
164
183
  }}
@@ -170,10 +189,24 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
170
189
  onDragLeave={() => setDrag(false)}
171
190
  onDrop={onDropFile}
172
191
  >
173
- <div className="input-image-dropzone-content">
174
- <Icon icon={drag ? "solid/hand-holding" : "solid/images"} className="text-3xl" />
175
- <p className="input-image-dropzone-text">{drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar"}</p>
176
- </div>
192
+ {preview && !containerAspect ? (
193
+ <div className="relative group w-full h-full flex items-center justify-center">
194
+ <img
195
+ src={preview}
196
+ alt="Preview"
197
+ className="w-full h-auto max-h-[400px] object-contain rounded-lg"
198
+ />
199
+ <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg">
200
+ <Icon icon="solid/images" className="text-3xl mb-2" />
201
+ <p className="text-xs text-center">Klik atau seret untuk ganti Gambar</p>
202
+ </div>
203
+ </div>
204
+ ) : (
205
+ <div className="input-image-dropzone-content">
206
+ <Icon icon={drag ? "solid/hand-holding" : "solid/images"} className="text-3xl" />
207
+ <p className="input-image-dropzone-text">{drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar"}</p>
208
+ </div>
209
+ )}
177
210
 
178
211
  <input
179
212
  id={randomId}
@@ -209,7 +242,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
209
242
  <div className="p-4">
210
243
  <CanvasCropper
211
244
  src={cropSrc}
212
- aspect={eval(aspect.replace(":", "/"))}
245
+ aspect={eval(currentAspect.replace(":", "/"))}
213
246
  onDone={onCropDone}
214
247
  />
215
248
  </div>
@@ -221,7 +254,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
221
254
  <div className="p-4">
222
255
  <CanvasCropper
223
256
  src={cropSrc}
224
- aspect={eval(aspect.replace(":", "/"))}
257
+ aspect={eval(currentAspect.replace(":", "/"))}
225
258
  onDone={onCropDone}
226
259
  />
227
260
  </div>
@@ -64,7 +64,7 @@ export type TableSupervisionProps = {
64
64
  shortcut ?: { key: string, description: string },
65
65
  on ?: (item: Record<string, any>) => boolean,
66
66
  } | ((
67
- row : object,
67
+ row : Record<string, any>,
68
68
  setModal ?: (type: "EDIT" | "DELETE") => void,
69
69
  setDataSelected ?: () => void,
70
70
  reset ?: () => void,