ar-design 0.2.49 → 0.2.50

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.
@@ -36,7 +36,7 @@
36
36
  }
37
37
  .ar-dnd > div.end-item {
38
38
  /* Sırasıyla; Ad, Süre, Hız, Gecikme Süresi, Tekrar Sayısı, Yön, Bitiş Süreci */
39
- animation: endItem ease-in-out 1000ms 0s 1 normal both;
39
+ animation: endItem ease-in-out 1s 0s 1 normal both;
40
40
  }
41
41
 
42
42
  .ar-dnd > div > .move {
@@ -55,3 +55,41 @@
55
55
  inset: 0;
56
56
  cursor: ns-resize;
57
57
  }
58
+
59
+ .ar-alias-panel {
60
+ visibility: hidden;
61
+ opacity: 0;
62
+ position: absolute;
63
+ background-color: var(--white);
64
+ width: 200px;
65
+ max-height: 250px;
66
+ border: solid 1px var(--gray-200);
67
+ border-radius: var(--border-radius-lg);
68
+ overflow-y: auto;
69
+ overflow-x: hidden;
70
+ box-shadow: 0 10px 15px -5px rgba(var(--black-rgb), 0.1);
71
+ /* Sırasıyla; Ad, Süre, Hız, Gecikme Süresi, Tekrar Sayısı, Yön, Bitiş Süreci */
72
+ animation: opened ease-in-out 250ms 0s 1 normal both;
73
+ }
74
+ .ar-alias-panel > ul > li {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 0 0.5rem;
78
+ padding: 0 1rem;
79
+ height: var(--input-height);
80
+ cursor: pointer;
81
+ }
82
+ .ar-alias-panel > ul > li:hover {
83
+ background-color: var(--gray-100);
84
+ }
85
+
86
+ @keyframes opened {
87
+ from {
88
+ visibility: hidden;
89
+ opacity: 0;
90
+ }
91
+ to {
92
+ visibility: visible;
93
+ opacity: 1;
94
+ }
95
+ }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
2
  import "../../../assets/css/components/form/button/styles.css";
3
3
  import IProps from "./IProps";
4
- declare const Button: React.ForwardRefExoticComponent<IProps & React.RefAttributes<HTMLButtonElement>>;
4
+ declare const Button: React.FC<IProps>;
5
5
  export default Button;
@@ -1,22 +1,9 @@
1
1
  "use client";
2
- import React, { forwardRef, useRef } from "react";
2
+ import React, { useRef } from "react";
3
3
  import "../../../assets/css/components/form/button/styles.css";
4
4
  import Utils from "../../../libs/infrastructure/shared/Utils";
5
5
  import Tooltip from "../../feedback/tooltip";
6
- // const Button: React.FC<IProps> = ({
7
- // children,
8
- // variant = "filled",
9
- // shape,
10
- // status = "primary",
11
- // border,
12
- // size = "normal",
13
- // tooltip,
14
- // position,
15
- // icon,
16
- // upperCase,
17
- // ...attributes
18
- // }) => {
19
- const Button = forwardRef(({ children, variant = "filled", shape, status = "primary", border, size = "normal", tooltip, position, icon, upperCase, ...attributes }, ref) => {
6
+ const Button = ({ children, variant = "filled", shape, status = "primary", border, size = "normal", tooltip, position, icon, upperCase, ...attributes }) => {
20
7
  // refs
21
8
  const _button = useRef(null);
22
9
  const _buttonClassName = ["ar-button"];
@@ -29,7 +16,7 @@ const Button = forwardRef(({ children, variant = "filled", shape, status = "prim
29
16
  _buttonClassName.push(position.type);
30
17
  _buttonClassName.push(position.inset.map((_inset) => _inset).join(" "));
31
18
  }
32
- const buttonElement = (React.createElement("button", { ref: ref ?? _button, ...attributes, className: _buttonClassName.map((c) => c).join(" "), onClick: (event) => {
19
+ const buttonElement = (React.createElement("button", { ref: _button, ...attributes, className: _buttonClassName.map((c) => c).join(" "), onClick: (event) => {
33
20
  // Disabled gelmesi durumunda işlem yapmasına izin verme...
34
21
  if (attributes.disabled)
35
22
  return;
@@ -39,7 +26,7 @@ const Button = forwardRef(({ children, variant = "filled", shape, status = "prim
39
26
  if (_current && !_current.classList.contains(addClass)) {
40
27
  // Sınıf ekleniyor...
41
28
  _current.classList.add(addClass);
42
- // Sınıf 750 milisaniye sonra kaldırlacak.
29
+ // Sınıf 500 milisaniye sonra kaldırlacak.
43
30
  setTimeout(() => _current.classList.remove(addClass), 750);
44
31
  }
45
32
  })();
@@ -49,6 +36,6 @@ const Button = forwardRef(({ children, variant = "filled", shape, status = "prim
49
36
  icon?.element,
50
37
  React.createElement("span", null, !shape ? (typeof children === "string" && upperCase ? children.toLocaleUpperCase() : children) : ""))));
51
38
  return !tooltip ? (buttonElement) : (React.createElement(Tooltip, { text: tooltip.text, direction: tooltip.direction }, buttonElement));
52
- });
39
+ };
53
40
  Button.displayName = "Button";
54
41
  export default Button;
@@ -1,8 +1,16 @@
1
1
  import { IValidation } from "../../../libs/types/IGlobalProps";
2
- interface IProps extends IValidation {
2
+ interface IProps<T> extends IValidation {
3
3
  name?: string;
4
4
  value?: string;
5
5
  onChange: (value?: string) => void;
6
+ dynamicList?: {
7
+ render: {
8
+ display: keyof T;
9
+ items: T[];
10
+ };
11
+ triggerKey?: string;
12
+ onTagged?: (tagged: any[]) => void;
13
+ };
6
14
  placeholder?: string;
7
15
  height?: number;
8
16
  multilang?: boolean;
@@ -1,5 +1,5 @@
1
1
  import "../../../assets/css/components/form/text-editor/styles.css";
2
2
  import IProps from "./IProps";
3
3
  import React from "react";
4
- declare const TextEditor: React.FC<IProps>;
4
+ declare const TextEditor: <T extends object>({ name, value, onChange, dynamicList, height, validation, }: IProps<T>) => React.JSX.Element;
5
5
  export default TextEditor;
@@ -4,15 +4,28 @@ import { ARIcon } from "../../icons";
4
4
  import Button from "../button";
5
5
  import React, { useEffect, useRef, useState } from "react";
6
6
  import Utils from "../../../libs/infrastructure/shared/Utils";
7
- const TextEditor = ({ name, value, onChange, placeholder, height, multilang, validation }) => {
7
+ import ReactDOM from "react-dom";
8
+ const TextEditor = ({ name, value, onChange, dynamicList,
9
+ // placeholder,
10
+ height,
11
+ // multilang,
12
+ validation, }) => {
8
13
  // refs
9
14
  const _container = useRef(null);
10
15
  const _arIframe = useRef(null);
11
16
  const _onChange = useRef(onChange);
12
17
  const _onChangeTimeOut = useRef(null);
18
+ // refs -> Alias Panel
19
+ const _target = useRef(null);
20
+ const _arAliasPanel = useRef(null);
13
21
  // states
14
22
  const [iframe, setIframe] = useState(null);
15
23
  const [iframeDocument, setIframeDocument] = useState(undefined);
24
+ // states -> Data
25
+ const [tagged, setTagged] = useState([]);
26
+ // states -> Alias Panel
27
+ const [atRect, setAtRect] = useState(null);
28
+ const [filtered, setFiltered] = useState(null);
16
29
  // variables
17
30
  const toolbarButtons = [
18
31
  { command: "bold", icon: "Bold", tooltip: `Bold (${Utils.GetOSShortCutIcons()} + B)` },
@@ -35,7 +48,10 @@ const TextEditor = ({ name, value, onChange, placeholder, height, multilang, val
35
48
  iframeDoc.execCommand(command, true, undefined);
36
49
  };
37
50
  const handleFocus = () => _arIframe.current?.classList.add("focused");
38
- const handleBlur = () => _arIframe.current?.classList.remove("focused");
51
+ const handleBlur = () => {
52
+ _arIframe.current?.classList.remove("focused");
53
+ // setAtRect(null);
54
+ };
39
55
  const handleMouseDown = () => {
40
56
  // Resizebar a tıklandığında iframe içerisinde bulunan window'un event listenerı olmadığı için orada resize çalışmayacaktır.
41
57
  // Bu yüzden önüne bir duvar örüyoruz ve mevcut sayfanın window'unda işlem yapmaya devam ediyor.
@@ -55,6 +71,44 @@ const TextEditor = ({ name, value, onChange, placeholder, height, multilang, val
55
71
  _arIframe.current.style.height = `${height}px`;
56
72
  }
57
73
  };
74
+ // methods -> Alias Panel
75
+ const handleBackSpaceKeydown = (event) => {
76
+ const key = event.key;
77
+ if (key === "Backspace" || key === "Delete") {
78
+ const selection = _arIframe.current?.contentDocument?.getSelection();
79
+ if (!selection || selection.rangeCount === 0)
80
+ return;
81
+ const range = selection.getRangeAt(0);
82
+ // 1. Çoklu seçim varsa: clone edip span'ları bul.
83
+ const contents = range.cloneContents();
84
+ const multiSpans = contents.querySelectorAll("span[data-tag]");
85
+ if (multiSpans.length > 0) {
86
+ event.preventDefault();
87
+ const tagsToRemove = [];
88
+ multiSpans.forEach((span) => {
89
+ const tag = span.getAttribute("data-tag");
90
+ if (tag)
91
+ tagsToRemove.push(tag);
92
+ });
93
+ // DOM'dan temizle
94
+ range.deleteContents();
95
+ // State'ten temizle
96
+ setTagged((prev) => prev.filter((x) => tagsToRemove.every((tag) => !JSON.stringify(x).includes(tag))));
97
+ return;
98
+ }
99
+ // 2. Tekli seçim: caret bir span içindeyse sil
100
+ const node = selection.anchorNode;
101
+ const container = node?.parentElement;
102
+ if (container?.tagName === "SPAN" && container.dataset.tag) {
103
+ event.preventDefault();
104
+ const tag = container.dataset.tag;
105
+ // DOM'dan sil
106
+ container.remove();
107
+ // State'ten kaldır
108
+ setTagged((prev) => prev.filter((x) => !JSON.stringify(x).includes(tag ?? "")));
109
+ }
110
+ }
111
+ };
58
112
  // useEffects
59
113
  useEffect(() => {
60
114
  // Iframe Document yüklendikten sonra çalışacaktır.
@@ -94,7 +148,32 @@ const TextEditor = ({ name, value, onChange, placeholder, height, multilang, val
94
148
  if (_onChangeTimeOut.current)
95
149
  clearTimeout(_onChangeTimeOut.current);
96
150
  _onChangeTimeOut.current = setTimeout(() => {
97
- mutationsList.forEach(() => {
151
+ mutationsList.forEach((record) => {
152
+ const target = record.target;
153
+ _target.current = record.target;
154
+ if (dynamicList) {
155
+ if (target.nodeType === Node.TEXT_NODE) {
156
+ const text = target.textContent ?? "";
157
+ const atIndex = text.lastIndexOf(dynamicList?.triggerKey ?? "@");
158
+ if (atIndex !== -1) {
159
+ const afterAt = text.slice(atIndex + 1); // @ sonrası metin.
160
+ const hasWhitespace = /\s/.test(afterAt);
161
+ if (!hasWhitespace) {
162
+ const selection = _iframeDocument?.getSelection();
163
+ if (selection && selection.rangeCount > 0) {
164
+ const range = selection.getRangeAt(0).cloneRange();
165
+ const rect = range.getBoundingClientRect();
166
+ range.collapse(true);
167
+ setAtRect(rect);
168
+ setFiltered(afterAt);
169
+ return;
170
+ }
171
+ }
172
+ }
173
+ // Eğer @ yoksa ya da boşluk varsa paneli kapat.
174
+ setAtRect(null);
175
+ }
176
+ }
98
177
  _iframeDocument?.body.innerHTML === "<br>"
99
178
  ? _onChange.current(undefined)
100
179
  : _onChange.current(_iframeDocument.body.innerHTML);
@@ -105,12 +184,21 @@ const TextEditor = ({ name, value, onChange, placeholder, height, multilang, val
105
184
  observer.observe(_iframeDocument.body, { childList: true, subtree: true, characterData: true, attributes: true });
106
185
  _iframeDocument.body.addEventListener("focus", handleFocus);
107
186
  _iframeDocument.body.addEventListener("blur", handleBlur);
187
+ if (dynamicList) {
188
+ _iframeDocument.body.addEventListener("keydown", handleBackSpaceKeydown);
189
+ }
108
190
  return () => {
109
191
  observer.disconnect();
110
- _iframeDocument?.body.removeEventListener("focus", handleFocus);
111
- _iframeDocument?.body.removeEventListener("blur", handleBlur);
192
+ _iframeDocument.body.removeEventListener("focus", handleFocus);
193
+ _iframeDocument.body.removeEventListener("blur", handleBlur);
194
+ if (dynamicList) {
195
+ _iframeDocument.body.removeEventListener("keydown", handleBackSpaceKeydown);
196
+ }
112
197
  };
113
198
  }, [iframe]);
199
+ useEffect(() => {
200
+ dynamicList?.onTagged && dynamicList?.onTagged(tagged);
201
+ }, [tagged]);
114
202
  useEffect(() => {
115
203
  if (!_arIframe.current)
116
204
  return;
@@ -128,6 +216,67 @@ const TextEditor = ({ name, value, onChange, placeholder, height, multilang, val
128
216
  text: tooltip,
129
217
  }, onClick: () => execCommand(command) })))),
130
218
  React.createElement("div", { className: "resize", onMouseDown: handleMouseDown }),
131
- validation?.text && React.createElement("span", { className: "validation" }, validation.text)));
219
+ validation?.text && React.createElement("span", { className: "validation" }, validation.text),
220
+ dynamicList &&
221
+ atRect &&
222
+ ReactDOM.createPortal(React.createElement("div", { ref: _arAliasPanel, className: "ar-alias-panel", style: {
223
+ top: (_arIframe.current?.getBoundingClientRect().top ?? 0) + atRect.top + 20,
224
+ left: (_arIframe.current?.getBoundingClientRect().left ?? 0) + atRect.left,
225
+ }, onClick: () => {
226
+ setAtRect(null);
227
+ } },
228
+ React.createElement("ul", null, dynamicList &&
229
+ dynamicList.render.items
230
+ // .filter((fItem) => !tagged.some((t: T) => JSON.stringify(fItem) === JSON.stringify(t)))
231
+ .filter((item) => {
232
+ const displayText = item[dynamicList.render.display] ?? "";
233
+ return displayText.toLowerCase().includes(filtered.toLowerCase());
234
+ })
235
+ .map((item, index) => (React.createElement("li", { key: index, onClick: (event) => {
236
+ event.stopPropagation();
237
+ const selection = iframeDocument?.getSelection();
238
+ const target = _target.current;
239
+ if (selection && selection.rangeCount > 0 && target && target.nodeType === Node.TEXT_NODE) {
240
+ const text = target.textContent ?? "";
241
+ const atIndex = text.lastIndexOf(dynamicList?.triggerKey ?? "@");
242
+ if (atIndex !== -1) {
243
+ const range = selection.getRangeAt(0).cloneRange();
244
+ range.setStart(target, atIndex);
245
+ range.setEnd(target, text.length);
246
+ range.deleteContents();
247
+ const itemText = item[dynamicList.render.display] ?? "";
248
+ const span = iframeDocument?.createElement("span");
249
+ const spaceNode = iframeDocument?.createTextNode(" \u200B");
250
+ if (span && spaceNode && iframeDocument) {
251
+ span.setAttribute("data-tag", `${itemText}`);
252
+ span.style.backgroundColor = "rgba(114, 15, 103, .1)";
253
+ span.style.padding = "0 5px";
254
+ span.style.borderRadius = "2px";
255
+ span.style.color = "#720f67";
256
+ span.style.fontWeight = "bold";
257
+ span.textContent = `@${itemText}`;
258
+ // Yeni bir wrapper fragment oluştur
259
+ const fragment = iframeDocument.createDocumentFragment();
260
+ fragment.appendChild(span);
261
+ fragment.appendChild(spaceNode); // görünmez boş node, ama yazılabilir
262
+ range.insertNode(fragment);
263
+ // Cursor'u spaceNode’un sonuna yerleştir
264
+ const newRange = iframeDocument.createRange();
265
+ newRange.setStart(spaceNode, spaceNode.length);
266
+ newRange.collapse(true);
267
+ selection.removeAllRanges();
268
+ selection.addRange(newRange);
269
+ // Focus zaten varsa sorun olmayacak
270
+ const activeEl = iframeDocument?.activeElement;
271
+ activeEl?.focus();
272
+ }
273
+ setAtRect(null);
274
+ setTagged((prev) => {
275
+ const exists = prev.some((i) => JSON.stringify(i) === JSON.stringify(item));
276
+ return exists ? prev : [...prev, item];
277
+ });
278
+ }
279
+ }
280
+ } }, item[dynamicList.render.display] ?? ""))))), document.body)));
132
281
  };
133
282
  export default TextEditor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ar-design",
3
- "version": "0.2.49",
3
+ "version": "0.2.50",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",