@skalfa/skalfa-component 1.0.28 → 1.0.30
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.
- package/dist/input/InputContent.component.js +120 -64
- package/package.json +1 -1
- package/src/input/InputContent.component.tsx +163 -108
|
@@ -11,6 +11,7 @@ const Button_component_1 = require("../button/Button.component");
|
|
|
11
11
|
exports.DEFAULT_TOOLBAR_CONTROLS = [
|
|
12
12
|
"HEADER",
|
|
13
13
|
"TEXT_SIZE",
|
|
14
|
+
"COLOR",
|
|
14
15
|
"BOLD",
|
|
15
16
|
"ITALIC",
|
|
16
17
|
"UNDERLINE",
|
|
@@ -20,7 +21,6 @@ exports.DEFAULT_TOOLBAR_CONTROLS = [
|
|
|
20
21
|
"ALIGN_RIGHT",
|
|
21
22
|
"ALIGN_JUSTIFY",
|
|
22
23
|
"LINK",
|
|
23
|
-
"COLOR",
|
|
24
24
|
"LIST_BULLET",
|
|
25
25
|
"LIST_NUMBER",
|
|
26
26
|
"DIVIDER",
|
|
@@ -308,7 +308,102 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
|
|
|
308
308
|
saveSelection();
|
|
309
309
|
handleEditorChange();
|
|
310
310
|
}, [restoreSelection, saveSelection, handleEditorChange]);
|
|
311
|
+
const updateActiveStates = (0, react_1.useCallback)(() => {
|
|
312
|
+
const sel = window.getSelection();
|
|
313
|
+
let inH2 = false;
|
|
314
|
+
if (sel && sel.rangeCount > 0) {
|
|
315
|
+
let node = sel.anchorNode;
|
|
316
|
+
while (node && node !== editorRef.current) {
|
|
317
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "H2") {
|
|
318
|
+
inH2 = true;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
node = node.parentNode;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
setActiveStates({
|
|
325
|
+
header: inH2,
|
|
326
|
+
bold: isCommandActive("bold"),
|
|
327
|
+
italic: isCommandActive("italic"),
|
|
328
|
+
underline: isCommandActive("underline"),
|
|
329
|
+
strikeThrough: isCommandActive("strikeThrough"),
|
|
330
|
+
justifyLeft: isCommandActive("justifyLeft"),
|
|
331
|
+
justifyCenter: isCommandActive("justifyCenter"),
|
|
332
|
+
justifyRight: isCommandActive("justifyRight"),
|
|
333
|
+
justifyFull: isCommandActive("justifyFull"),
|
|
334
|
+
insertUnorderedList: isCommandActive("insertUnorderedList"),
|
|
335
|
+
insertOrderedList: isCommandActive("insertOrderedList"),
|
|
336
|
+
});
|
|
337
|
+
if (sel && sel.rangeCount > 0) {
|
|
338
|
+
let node = sel.anchorNode;
|
|
339
|
+
let detectedColor = "normal";
|
|
340
|
+
let detectedSize = null;
|
|
341
|
+
while (node && node !== editorRef.current) {
|
|
342
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
343
|
+
const el = node;
|
|
344
|
+
if (el.dataset.color && detectedColor === "normal") {
|
|
345
|
+
detectedColor = el.dataset.color;
|
|
346
|
+
}
|
|
347
|
+
if (el.dataset.size && detectedSize === null) {
|
|
348
|
+
const parsed = parseInt(el.dataset.size);
|
|
349
|
+
if (!isNaN(parsed))
|
|
350
|
+
detectedSize = parsed;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
node = node.parentNode;
|
|
354
|
+
}
|
|
355
|
+
setActiveColor(detectedColor);
|
|
356
|
+
if (detectedSize !== null) {
|
|
357
|
+
setTextSize(detectedSize);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}, [isCommandActive]);
|
|
361
|
+
(0, react_1.useEffect)(() => {
|
|
362
|
+
_utils_1.shortcut.register("ctrl+b", () => handleBold(), "Bold text");
|
|
363
|
+
_utils_1.shortcut.register("ctrl+i", () => handleItalic(), "Italic text");
|
|
364
|
+
_utils_1.shortcut.register("ctrl+u", () => handleUnderline(), "Underline text");
|
|
365
|
+
_utils_1.shortcut.register("ctrl+shift+x", () => handleStrikethrough(), "Strikethrough text");
|
|
366
|
+
_utils_1.shortcut.register("ctrl+k", () => handleLink(), "Insert link");
|
|
367
|
+
return () => {
|
|
368
|
+
_utils_1.shortcut.unregister("ctrl+b");
|
|
369
|
+
_utils_1.shortcut.unregister("ctrl+i");
|
|
370
|
+
_utils_1.shortcut.unregister("ctrl+u");
|
|
371
|
+
_utils_1.shortcut.unregister("ctrl+shift+x");
|
|
372
|
+
_utils_1.shortcut.unregister("ctrl+k");
|
|
373
|
+
};
|
|
374
|
+
}, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink]);
|
|
311
375
|
const handleKeyDown = (0, react_1.useCallback)((e) => {
|
|
376
|
+
const isMod = e.ctrlKey || e.metaKey;
|
|
377
|
+
const key = e.key.toLowerCase();
|
|
378
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "b") {
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
handleBold();
|
|
381
|
+
updateActiveStates();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "i") {
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
handleItalic();
|
|
387
|
+
updateActiveStates();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "u") {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
handleUnderline();
|
|
393
|
+
updateActiveStates();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (isMod && e.shiftKey && !e.altKey && key === "x") {
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
handleStrikethrough();
|
|
399
|
+
updateActiveStates();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "k") {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
handleLink();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
312
407
|
if (e.key === "Backspace") {
|
|
313
408
|
const sel = window.getSelection();
|
|
314
409
|
if (sel && sel.rangeCount > 0 && sel.isCollapsed) {
|
|
@@ -362,75 +457,41 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
|
|
|
362
457
|
}
|
|
363
458
|
}
|
|
364
459
|
}
|
|
365
|
-
}, [handleEditorChange]);
|
|
366
|
-
const updateActiveStates = (0, react_1.useCallback)(() => {
|
|
367
|
-
const sel = window.getSelection();
|
|
368
|
-
let inH2 = false;
|
|
369
|
-
if (sel && sel.rangeCount > 0) {
|
|
370
|
-
let node = sel.anchorNode;
|
|
371
|
-
while (node && node !== editorRef.current) {
|
|
372
|
-
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "H2") {
|
|
373
|
-
inH2 = true;
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
node = node.parentNode;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
setActiveStates({
|
|
380
|
-
header: inH2,
|
|
381
|
-
bold: isCommandActive("bold"),
|
|
382
|
-
italic: isCommandActive("italic"),
|
|
383
|
-
underline: isCommandActive("underline"),
|
|
384
|
-
strikeThrough: isCommandActive("strikeThrough"),
|
|
385
|
-
justifyLeft: isCommandActive("justifyLeft"),
|
|
386
|
-
justifyCenter: isCommandActive("justifyCenter"),
|
|
387
|
-
justifyRight: isCommandActive("justifyRight"),
|
|
388
|
-
justifyFull: isCommandActive("justifyFull"),
|
|
389
|
-
insertUnorderedList: isCommandActive("insertUnorderedList"),
|
|
390
|
-
insertOrderedList: isCommandActive("insertOrderedList"),
|
|
391
|
-
});
|
|
392
|
-
if (sel && sel.rangeCount > 0) {
|
|
393
|
-
let node = sel.anchorNode;
|
|
394
|
-
let detectedColor = "normal";
|
|
395
|
-
let detectedSize = null;
|
|
396
|
-
while (node && node !== editorRef.current) {
|
|
397
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
398
|
-
const el = node;
|
|
399
|
-
if (el.dataset.color && detectedColor === "normal") {
|
|
400
|
-
detectedColor = el.dataset.color;
|
|
401
|
-
}
|
|
402
|
-
if (el.dataset.size && detectedSize === null) {
|
|
403
|
-
const parsed = parseInt(el.dataset.size);
|
|
404
|
-
if (!isNaN(parsed))
|
|
405
|
-
detectedSize = parsed;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
node = node.parentNode;
|
|
409
|
-
}
|
|
410
|
-
setActiveColor(detectedColor);
|
|
411
|
-
if (detectedSize !== null) {
|
|
412
|
-
setTextSize(detectedSize);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}, [isCommandActive]);
|
|
460
|
+
}, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink, handleEditorChange, updateActiveStates]);
|
|
416
461
|
const renderControlItem = (0, react_1.useCallback)((item, index) => {
|
|
417
462
|
if (typeof item !== "string") {
|
|
418
463
|
return (0, jsx_runtime_1.jsx)("div", { children: item }, index);
|
|
419
464
|
}
|
|
420
465
|
switch (item) {
|
|
421
|
-
case "HEADER":
|
|
422
|
-
|
|
466
|
+
// case "HEADER":
|
|
467
|
+
// return (
|
|
468
|
+
// <ButtonComponent
|
|
469
|
+
// key={index}
|
|
470
|
+
// variant={activeStates.header ? "light" : "simple"}
|
|
471
|
+
// paint="primary"
|
|
472
|
+
// size="sm"
|
|
473
|
+
// icon="solid/text-h"
|
|
474
|
+
// tips="Header"
|
|
475
|
+
// className={cn("toolbar-btn", activeStates.header && "toolbar-btn-active")}
|
|
476
|
+
// onClick={(e: any) => { e?.preventDefault?.(); handleHeader(); }}
|
|
477
|
+
// />
|
|
478
|
+
// );
|
|
423
479
|
case "TEXT_SIZE":
|
|
424
480
|
case "FONT_SIZE":
|
|
425
481
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-0.5", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: "simple", paint: "primary", size: "sm", icon: "solid/minus", tips: "Decrease Font Size", className: "toolbar-btn", onClick: (e) => { e?.preventDefault?.(); handleFontSizeChange(textSize - 1); } }), (0, jsx_runtime_1.jsx)("input", { type: "number", value: textSize, onChange: (e) => handleFontSizeChange(parseInt(e.target.value) || 14), onFocus: () => saveSelection(), className: "w-10 h-7 text-xs text-center border border-stroke rounded focus:outline-none focus:border-primary bg-background text-foreground select-none" }), (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: "simple", paint: "primary", size: "sm", icon: "solid/plus", tips: "Increase Font Size", className: "toolbar-btn", onClick: (e) => { e?.preventDefault?.(); handleFontSizeChange(textSize + 1); } })] }, index));
|
|
482
|
+
case "COLOR":
|
|
483
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeColor !== "normal" ? "light" : "simple", paint: "primary", size: "sm", label: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("div", { className: "skcontent-color-dot", style: { backgroundColor: ContentWrapper_component_1.COLOR_MAP[activeColor]?.css || ContentWrapper_component_1.COLOR_MAP.normal.css } }) }), tips: `Text Color (${ContentWrapper_component_1.COLOR_MAP[activeColor]?.label || "Normal"})`, className: (0, _utils_1.cn)("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); } }), showColorPicker && ((0, jsx_runtime_1.jsx)("div", { className: "skcontent-dropdown skcontent-color-picker", children: Object.entries(ContentWrapper_component_1.COLOR_MAP).map(([key, info]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", title: info.label, className: (0, _utils_1.cn)("skcontent-color-dot", activeColor === key && "scale-125 border-2 border-primary"), style: { backgroundColor: info.css }, onMouseDown: (e) => {
|
|
484
|
+
e.preventDefault();
|
|
485
|
+
handleColor(key);
|
|
486
|
+
} }, key))) }))] }, index));
|
|
426
487
|
case "BOLD":
|
|
427
|
-
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.bold ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-b", tips: "Bold", className: (0, _utils_1.cn)("toolbar-btn", activeStates.bold && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleBold(); } }, index));
|
|
488
|
+
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.bold ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-b", tips: "Bold (Ctrl+B)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.bold && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleBold(); } }, index));
|
|
428
489
|
case "ITALIC":
|
|
429
|
-
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.italic ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-italic", tips: "Italic", className: (0, _utils_1.cn)("toolbar-btn", activeStates.italic && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleItalic(); } }, index));
|
|
490
|
+
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.italic ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-italic", tips: "Italic (Ctrl+I)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.italic && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleItalic(); } }, index));
|
|
430
491
|
case "UNDERLINE":
|
|
431
|
-
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.underline ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-underline", tips: "Underline", className: (0, _utils_1.cn)("toolbar-btn", activeStates.underline && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleUnderline(); } }, index));
|
|
492
|
+
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.underline ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-underline", tips: "Underline (Ctrl+U)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.underline && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleUnderline(); } }, index));
|
|
432
493
|
case "STRIKETHROUGH":
|
|
433
|
-
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.strikeThrough ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-slash", tips: "Strikethrough", className: (0, _utils_1.cn)("toolbar-btn", activeStates.strikeThrough && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleStrikethrough(); } }, index));
|
|
494
|
+
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.strikeThrough ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-slash", tips: "Strikethrough (Ctrl+Shift+X)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.strikeThrough && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleStrikethrough(); } }, index));
|
|
434
495
|
case "ALIGN_LEFT":
|
|
435
496
|
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.justifyLeft ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-left", tips: "Align Left", className: (0, _utils_1.cn)("toolbar-btn", activeStates.justifyLeft && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleAlign("left"); } }, index));
|
|
436
497
|
case "ALIGN_CENTER":
|
|
@@ -440,7 +501,7 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
|
|
|
440
501
|
case "ALIGN_JUSTIFY":
|
|
441
502
|
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.justifyFull ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-justify", tips: "Align Justify", className: (0, _utils_1.cn)("toolbar-btn", activeStates.justifyFull && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleAlign("justify"); } }, index));
|
|
442
503
|
case "LINK":
|
|
443
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: showLinkInput ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/link", tips: "Link", className: (0, _utils_1.cn)("toolbar-btn", showLinkInput && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleLink(); } }), showLinkInput && ((0, jsx_runtime_1.jsxs)("div", { className: "skcontent-dropdown skcontent-link-input", children: [(0, jsx_runtime_1.jsx)("input", { type: "url", placeholder: "https://...", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), onKeyDown: (e) => {
|
|
504
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: showLinkInput ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/link", tips: "Link (Ctrl+K)", className: (0, _utils_1.cn)("toolbar-btn", showLinkInput && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleLink(); } }), showLinkInput && ((0, jsx_runtime_1.jsxs)("div", { className: "skcontent-dropdown skcontent-link-input", children: [(0, jsx_runtime_1.jsx)("input", { type: "url", placeholder: "https://...", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), onKeyDown: (e) => {
|
|
444
505
|
if (e.key === "Enter") {
|
|
445
506
|
e.preventDefault();
|
|
446
507
|
handleLinkSubmit();
|
|
@@ -449,11 +510,6 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
|
|
|
449
510
|
setShowLinkInput(false);
|
|
450
511
|
}
|
|
451
512
|
}, className: "skcontent-link-url-input", autoFocus: true }), (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: "solid", paint: "primary", size: "xs", icon: "solid/check", onClick: (e) => { e?.preventDefault?.(); handleLinkSubmit(); } })] }))] }, index));
|
|
452
|
-
case "COLOR":
|
|
453
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeColor !== "normal" ? "light" : "simple", paint: "primary", size: "sm", label: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("div", { className: "skcontent-color-dot", style: { backgroundColor: ContentWrapper_component_1.COLOR_MAP[activeColor]?.css || ContentWrapper_component_1.COLOR_MAP.normal.css } }) }), tips: `Text Color (${ContentWrapper_component_1.COLOR_MAP[activeColor]?.label || "Normal"})`, className: (0, _utils_1.cn)("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); } }), showColorPicker && ((0, jsx_runtime_1.jsx)("div", { className: "skcontent-dropdown skcontent-color-picker", children: Object.entries(ContentWrapper_component_1.COLOR_MAP).map(([key, info]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", title: info.label, className: (0, _utils_1.cn)("skcontent-color-dot", activeColor === key && "scale-125 border-2 border-primary"), style: { backgroundColor: info.css }, onMouseDown: (e) => {
|
|
454
|
-
e.preventDefault();
|
|
455
|
-
handleColor(key);
|
|
456
|
-
} }, key))) }))] }, index));
|
|
457
513
|
case "LIST_BULLET":
|
|
458
514
|
case "BULLET_LIST":
|
|
459
515
|
return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.insertUnorderedList ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/list-bullet", tips: "Bullet List", className: (0, _utils_1.cn)("toolbar-btn", activeStates.insertUnorderedList && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleBulletList(); } }, index));
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ReactNode, Ref, useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { Icon } from "@skalfa/skalfa-icon";
|
|
5
|
-
import { cn, pcn, useInputHandler, useInputRandomId, useValidation, ValidationRules } from "@utils";
|
|
5
|
+
import { cn, pcn, shortcut, useInputHandler, useInputRandomId, useValidation, ValidationRules } from "@utils";
|
|
6
6
|
import { COLOR_MAP, parseContentToHtml, parseHtmlToContent } from "../wrap/ContentWrapper.component";
|
|
7
7
|
import { ButtonComponent } from "../button/Button.component";
|
|
8
8
|
|
|
@@ -34,6 +34,7 @@ export type ToolbarControlItem = ToolbarControlName | ReactNode;
|
|
|
34
34
|
export const DEFAULT_TOOLBAR_CONTROLS: ToolbarControlItem[] = [
|
|
35
35
|
"HEADER",
|
|
36
36
|
"TEXT_SIZE",
|
|
37
|
+
"COLOR",
|
|
37
38
|
"BOLD",
|
|
38
39
|
"ITALIC",
|
|
39
40
|
"UNDERLINE",
|
|
@@ -43,7 +44,6 @@ export const DEFAULT_TOOLBAR_CONTROLS: ToolbarControlItem[] = [
|
|
|
43
44
|
"ALIGN_RIGHT",
|
|
44
45
|
"ALIGN_JUSTIFY",
|
|
45
46
|
"LINK",
|
|
46
|
-
"COLOR",
|
|
47
47
|
"LIST_BULLET",
|
|
48
48
|
"LIST_NUMBER",
|
|
49
49
|
"DIVIDER",
|
|
@@ -408,7 +408,114 @@ export function InputContentComponent({
|
|
|
408
408
|
handleEditorChange();
|
|
409
409
|
}, [restoreSelection, saveSelection, handleEditorChange]);
|
|
410
410
|
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
const updateActiveStates = useCallback(() => {
|
|
414
|
+
const sel = window.getSelection();
|
|
415
|
+
let inH2 = false;
|
|
416
|
+
if (sel && sel.rangeCount > 0) {
|
|
417
|
+
let node: Node | null = sel.anchorNode;
|
|
418
|
+
while (node && node !== editorRef.current) {
|
|
419
|
+
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "H2") {
|
|
420
|
+
inH2 = true;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
node = node.parentNode;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
setActiveStates({
|
|
428
|
+
header: inH2,
|
|
429
|
+
bold: isCommandActive("bold"),
|
|
430
|
+
italic: isCommandActive("italic"),
|
|
431
|
+
underline: isCommandActive("underline"),
|
|
432
|
+
strikeThrough: isCommandActive("strikeThrough"),
|
|
433
|
+
justifyLeft: isCommandActive("justifyLeft"),
|
|
434
|
+
justifyCenter: isCommandActive("justifyCenter"),
|
|
435
|
+
justifyRight: isCommandActive("justifyRight"),
|
|
436
|
+
justifyFull: isCommandActive("justifyFull"),
|
|
437
|
+
insertUnorderedList: isCommandActive("insertUnorderedList"),
|
|
438
|
+
insertOrderedList: isCommandActive("insertOrderedList"),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (sel && sel.rangeCount > 0) {
|
|
442
|
+
let node: Node | null = sel.anchorNode;
|
|
443
|
+
let detectedColor = "normal";
|
|
444
|
+
let detectedSize: number | null = null;
|
|
445
|
+
while (node && node !== editorRef.current) {
|
|
446
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
447
|
+
const el = node as HTMLElement;
|
|
448
|
+
if (el.dataset.color && detectedColor === "normal") {
|
|
449
|
+
detectedColor = el.dataset.color;
|
|
450
|
+
}
|
|
451
|
+
if (el.dataset.size && detectedSize === null) {
|
|
452
|
+
const parsed = parseInt(el.dataset.size);
|
|
453
|
+
if (!isNaN(parsed)) detectedSize = parsed;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
node = node.parentNode;
|
|
457
|
+
}
|
|
458
|
+
setActiveColor(detectedColor);
|
|
459
|
+
if (detectedSize !== null) {
|
|
460
|
+
setTextSize(detectedSize);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}, [isCommandActive]);
|
|
464
|
+
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
shortcut.register("ctrl+b", () => handleBold(), "Bold text");
|
|
467
|
+
shortcut.register("ctrl+i", () => handleItalic(), "Italic text");
|
|
468
|
+
shortcut.register("ctrl+u", () => handleUnderline(), "Underline text");
|
|
469
|
+
shortcut.register("ctrl+shift+x", () => handleStrikethrough(), "Strikethrough text");
|
|
470
|
+
shortcut.register("ctrl+k", () => handleLink(), "Insert link");
|
|
471
|
+
|
|
472
|
+
return () => {
|
|
473
|
+
shortcut.unregister("ctrl+b");
|
|
474
|
+
shortcut.unregister("ctrl+i");
|
|
475
|
+
shortcut.unregister("ctrl+u");
|
|
476
|
+
shortcut.unregister("ctrl+shift+x");
|
|
477
|
+
shortcut.unregister("ctrl+k");
|
|
478
|
+
};
|
|
479
|
+
}, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink]);
|
|
480
|
+
|
|
411
481
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
482
|
+
const isMod = e.ctrlKey || e.metaKey;
|
|
483
|
+
const key = e.key.toLowerCase();
|
|
484
|
+
|
|
485
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "b") {
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
handleBold();
|
|
488
|
+
updateActiveStates();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "i") {
|
|
493
|
+
e.preventDefault();
|
|
494
|
+
handleItalic();
|
|
495
|
+
updateActiveStates();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "u") {
|
|
500
|
+
e.preventDefault();
|
|
501
|
+
handleUnderline();
|
|
502
|
+
updateActiveStates();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (isMod && e.shiftKey && !e.altKey && key === "x") {
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
handleStrikethrough();
|
|
509
|
+
updateActiveStates();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (isMod && !e.shiftKey && !e.altKey && key === "k") {
|
|
514
|
+
e.preventDefault();
|
|
515
|
+
handleLink();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
412
519
|
if (e.key === "Backspace") {
|
|
413
520
|
const sel = window.getSelection();
|
|
414
521
|
if (sel && sel.rangeCount > 0 && sel.isCollapsed) {
|
|
@@ -465,59 +572,7 @@ export function InputContentComponent({
|
|
|
465
572
|
}
|
|
466
573
|
}
|
|
467
574
|
}
|
|
468
|
-
}, [handleEditorChange]);
|
|
469
|
-
|
|
470
|
-
const updateActiveStates = useCallback(() => {
|
|
471
|
-
const sel = window.getSelection();
|
|
472
|
-
let inH2 = false;
|
|
473
|
-
if (sel && sel.rangeCount > 0) {
|
|
474
|
-
let node: Node | null = sel.anchorNode;
|
|
475
|
-
while (node && node !== editorRef.current) {
|
|
476
|
-
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "H2") {
|
|
477
|
-
inH2 = true;
|
|
478
|
-
break;
|
|
479
|
-
}
|
|
480
|
-
node = node.parentNode;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
setActiveStates({
|
|
485
|
-
header: inH2,
|
|
486
|
-
bold: isCommandActive("bold"),
|
|
487
|
-
italic: isCommandActive("italic"),
|
|
488
|
-
underline: isCommandActive("underline"),
|
|
489
|
-
strikeThrough: isCommandActive("strikeThrough"),
|
|
490
|
-
justifyLeft: isCommandActive("justifyLeft"),
|
|
491
|
-
justifyCenter: isCommandActive("justifyCenter"),
|
|
492
|
-
justifyRight: isCommandActive("justifyRight"),
|
|
493
|
-
justifyFull: isCommandActive("justifyFull"),
|
|
494
|
-
insertUnorderedList: isCommandActive("insertUnorderedList"),
|
|
495
|
-
insertOrderedList: isCommandActive("insertOrderedList"),
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
if (sel && sel.rangeCount > 0) {
|
|
499
|
-
let node: Node | null = sel.anchorNode;
|
|
500
|
-
let detectedColor = "normal";
|
|
501
|
-
let detectedSize: number | null = null;
|
|
502
|
-
while (node && node !== editorRef.current) {
|
|
503
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
504
|
-
const el = node as HTMLElement;
|
|
505
|
-
if (el.dataset.color && detectedColor === "normal") {
|
|
506
|
-
detectedColor = el.dataset.color;
|
|
507
|
-
}
|
|
508
|
-
if (el.dataset.size && detectedSize === null) {
|
|
509
|
-
const parsed = parseInt(el.dataset.size);
|
|
510
|
-
if (!isNaN(parsed)) detectedSize = parsed;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
node = node.parentNode;
|
|
514
|
-
}
|
|
515
|
-
setActiveColor(detectedColor);
|
|
516
|
-
if (detectedSize !== null) {
|
|
517
|
-
setTextSize(detectedSize);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}, [isCommandActive]);
|
|
575
|
+
}, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink, handleEditorChange, updateActiveStates]);
|
|
521
576
|
|
|
522
577
|
const renderControlItem = useCallback((item: ToolbarControlItem, index: number) => {
|
|
523
578
|
if (typeof item !== "string") {
|
|
@@ -525,19 +580,19 @@ export function InputContentComponent({
|
|
|
525
580
|
}
|
|
526
581
|
|
|
527
582
|
switch (item) {
|
|
528
|
-
case "HEADER":
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
583
|
+
// case "HEADER":
|
|
584
|
+
// return (
|
|
585
|
+
// <ButtonComponent
|
|
586
|
+
// key={index}
|
|
587
|
+
// variant={activeStates.header ? "light" : "simple"}
|
|
588
|
+
// paint="primary"
|
|
589
|
+
// size="sm"
|
|
590
|
+
// icon="solid/text-h"
|
|
591
|
+
// tips="Header"
|
|
592
|
+
// className={cn("toolbar-btn", activeStates.header && "toolbar-btn-active")}
|
|
593
|
+
// onClick={(e: any) => { e?.preventDefault?.(); handleHeader(); }}
|
|
594
|
+
// />
|
|
595
|
+
// );
|
|
541
596
|
case "TEXT_SIZE":
|
|
542
597
|
case "FONT_SIZE":
|
|
543
598
|
return (
|
|
@@ -569,6 +624,41 @@ export function InputContentComponent({
|
|
|
569
624
|
/>
|
|
570
625
|
</div>
|
|
571
626
|
);
|
|
627
|
+
case "COLOR":
|
|
628
|
+
return (
|
|
629
|
+
<div className="relative" key={index}>
|
|
630
|
+
<ButtonComponent
|
|
631
|
+
variant={activeColor !== "normal" ? "light" : "simple"}
|
|
632
|
+
paint="primary"
|
|
633
|
+
size="sm"
|
|
634
|
+
label={<div><div className="skcontent-color-dot" style={{ backgroundColor: COLOR_MAP[activeColor]?.css || COLOR_MAP.normal.css }} /></div>}
|
|
635
|
+
tips={`Text Color (${COLOR_MAP[activeColor]?.label || "Normal"})`}
|
|
636
|
+
className={cn("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active")}
|
|
637
|
+
onClick={(e: any) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); }}
|
|
638
|
+
/>
|
|
639
|
+
|
|
640
|
+
{showColorPicker && (
|
|
641
|
+
<div className="skcontent-dropdown skcontent-color-picker">
|
|
642
|
+
{Object.entries(COLOR_MAP).map(([key, info]) => (
|
|
643
|
+
<button
|
|
644
|
+
key={key}
|
|
645
|
+
type="button"
|
|
646
|
+
title={info.label}
|
|
647
|
+
className={cn(
|
|
648
|
+
"skcontent-color-dot",
|
|
649
|
+
activeColor === key && "scale-125 border-2 border-primary"
|
|
650
|
+
)}
|
|
651
|
+
style={{ backgroundColor: info.css }}
|
|
652
|
+
onMouseDown={(e) => {
|
|
653
|
+
e.preventDefault();
|
|
654
|
+
handleColor(key);
|
|
655
|
+
}}
|
|
656
|
+
/>
|
|
657
|
+
))}
|
|
658
|
+
</div>
|
|
659
|
+
)}
|
|
660
|
+
</div>
|
|
661
|
+
);
|
|
572
662
|
case "BOLD":
|
|
573
663
|
return (
|
|
574
664
|
<ButtonComponent
|
|
@@ -577,7 +667,7 @@ export function InputContentComponent({
|
|
|
577
667
|
paint="primary"
|
|
578
668
|
size="sm"
|
|
579
669
|
icon="solid/text-b"
|
|
580
|
-
tips="Bold"
|
|
670
|
+
tips="Bold (Ctrl+B)"
|
|
581
671
|
className={cn("toolbar-btn", activeStates.bold && "toolbar-btn-active")}
|
|
582
672
|
onClick={(e: any) => { e?.preventDefault?.(); handleBold(); }}
|
|
583
673
|
/>
|
|
@@ -590,7 +680,7 @@ export function InputContentComponent({
|
|
|
590
680
|
paint="primary"
|
|
591
681
|
size="sm"
|
|
592
682
|
icon="solid/text-italic"
|
|
593
|
-
tips="Italic"
|
|
683
|
+
tips="Italic (Ctrl+I)"
|
|
594
684
|
className={cn("toolbar-btn", activeStates.italic && "toolbar-btn-active")}
|
|
595
685
|
onClick={(e: any) => { e?.preventDefault?.(); handleItalic(); }}
|
|
596
686
|
/>
|
|
@@ -603,7 +693,7 @@ export function InputContentComponent({
|
|
|
603
693
|
paint="primary"
|
|
604
694
|
size="sm"
|
|
605
695
|
icon="solid/text-underline"
|
|
606
|
-
tips="Underline"
|
|
696
|
+
tips="Underline (Ctrl+U)"
|
|
607
697
|
className={cn("toolbar-btn", activeStates.underline && "toolbar-btn-active")}
|
|
608
698
|
onClick={(e: any) => { e?.preventDefault?.(); handleUnderline(); }}
|
|
609
699
|
/>
|
|
@@ -616,7 +706,7 @@ export function InputContentComponent({
|
|
|
616
706
|
paint="primary"
|
|
617
707
|
size="sm"
|
|
618
708
|
icon="solid/text-slash"
|
|
619
|
-
tips="Strikethrough"
|
|
709
|
+
tips="Strikethrough (Ctrl+Shift+X)"
|
|
620
710
|
className={cn("toolbar-btn", activeStates.strikeThrough && "toolbar-btn-active")}
|
|
621
711
|
onClick={(e: any) => { e?.preventDefault?.(); handleStrikethrough(); }}
|
|
622
712
|
/>
|
|
@@ -681,7 +771,7 @@ export function InputContentComponent({
|
|
|
681
771
|
paint="primary"
|
|
682
772
|
size="sm"
|
|
683
773
|
icon="solid/link"
|
|
684
|
-
tips="Link"
|
|
774
|
+
tips="Link (Ctrl+K)"
|
|
685
775
|
className={cn("toolbar-btn", showLinkInput && "toolbar-btn-active")}
|
|
686
776
|
onClick={(e: any) => { e?.preventDefault?.(); handleLink(); }}
|
|
687
777
|
/>
|
|
@@ -716,41 +806,6 @@ export function InputContentComponent({
|
|
|
716
806
|
)}
|
|
717
807
|
</div>
|
|
718
808
|
);
|
|
719
|
-
case "COLOR":
|
|
720
|
-
return (
|
|
721
|
-
<div className="relative" key={index}>
|
|
722
|
-
<ButtonComponent
|
|
723
|
-
variant={activeColor !== "normal" ? "light" : "simple"}
|
|
724
|
-
paint="primary"
|
|
725
|
-
size="sm"
|
|
726
|
-
label={<div><div className="skcontent-color-dot" style={{ backgroundColor: COLOR_MAP[activeColor]?.css || COLOR_MAP.normal.css }} /></div>}
|
|
727
|
-
tips={`Text Color (${COLOR_MAP[activeColor]?.label || "Normal"})`}
|
|
728
|
-
className={cn("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active")}
|
|
729
|
-
onClick={(e: any) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); }}
|
|
730
|
-
/>
|
|
731
|
-
|
|
732
|
-
{showColorPicker && (
|
|
733
|
-
<div className="skcontent-dropdown skcontent-color-picker">
|
|
734
|
-
{Object.entries(COLOR_MAP).map(([key, info]) => (
|
|
735
|
-
<button
|
|
736
|
-
key={key}
|
|
737
|
-
type="button"
|
|
738
|
-
title={info.label}
|
|
739
|
-
className={cn(
|
|
740
|
-
"skcontent-color-dot",
|
|
741
|
-
activeColor === key && "scale-125 border-2 border-primary"
|
|
742
|
-
)}
|
|
743
|
-
style={{ backgroundColor: info.css }}
|
|
744
|
-
onMouseDown={(e) => {
|
|
745
|
-
e.preventDefault();
|
|
746
|
-
handleColor(key);
|
|
747
|
-
}}
|
|
748
|
-
/>
|
|
749
|
-
))}
|
|
750
|
-
</div>
|
|
751
|
-
)}
|
|
752
|
-
</div>
|
|
753
|
-
);
|
|
754
809
|
case "LIST_BULLET":
|
|
755
810
|
case "BULLET_LIST":
|
|
756
811
|
return (
|