@umodeapporg/ui 0.4.1 → 0.6.0
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/components/Form/RichTextEditor.js +1 -0
- package/dist/components/Form/RichTextEditor.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.d.ts +3 -3
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +179 -150
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.d.ts +13 -0
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.js +94 -0
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.js.map +1 -0
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.d.ts +16 -0
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.js +79 -0
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.js.map +1 -0
- package/dist/components/UI/DataTable/bulkEditValues.d.ts +8 -0
- package/dist/components/UI/DataTable/bulkEditValues.js +13 -0
- package/dist/components/UI/DataTable/bulkEditValues.js.map +1 -0
- package/dist/components/UI/RichTextContent.js +4 -2
- package/dist/components/UI/RichTextContent.js.map +1 -1
- package/dist/helpers/richText.d.ts +9 -0
- package/dist/helpers/richText.js +37 -11
- package/dist/helpers/richText.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +13 -1
- package/dist/locales/en/ui.json.js +1 -1
- package/dist/locales/index.d.ts +24 -0
- package/dist/locales/pt/ui.json.d.ts +13 -1
- package/dist/locales/pt/ui.json.js +1 -1
- package/package.json +1 -1
- package/src/assets/styles/base.css +23 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IDataTableEditableConfig, IDataTableRow } from './types';
|
|
2
|
+
interface IDataTableBulkEditValueEditorProps<T extends IDataTableRow> {
|
|
3
|
+
editable: IDataTableEditableConfig<T>;
|
|
4
|
+
value: unknown;
|
|
5
|
+
onChange: (value: unknown) => void;
|
|
6
|
+
label?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
errorMessage?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A value editor derived from the column's `editable` descriptor, the same one
|
|
12
|
+
* the spreadsheet cell opens. Used by the "same value" mode (one editor for the
|
|
13
|
+
* whole scope) and by the per-item matrix (one editor per cell).
|
|
14
|
+
*/
|
|
15
|
+
declare const DataTableBulkEditValueEditor: <T extends IDataTableRow>({ editable, value, onChange, label, disabled, errorMessage, }: IDataTableBulkEditValueEditorProps<T>) => import("react").JSX.Element;
|
|
16
|
+
export default DataTableBulkEditValueEditor;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import DatePicker from "../../DateTime/DatePicker.js";
|
|
3
|
+
import ControlledInput from "../../Form/ControlledInput.js";
|
|
4
|
+
import { ControlledSelect } from "../../Form/ControlledSelect.js";
|
|
5
|
+
const DataTableBulkEditValueEditor = ({
|
|
6
|
+
editable,
|
|
7
|
+
value,
|
|
8
|
+
onChange,
|
|
9
|
+
label,
|
|
10
|
+
disabled,
|
|
11
|
+
errorMessage
|
|
12
|
+
}) => {
|
|
13
|
+
if (editable.renderEditor) {
|
|
14
|
+
return /* @__PURE__ */ jsx(Fragment, { children: editable.renderEditor({ value, onChange, disabled, label }) });
|
|
15
|
+
}
|
|
16
|
+
const arrayValue = Array.isArray(value) ? value.map(String) : [];
|
|
17
|
+
const stringValue = value == null || Array.isArray(value) ? "" : String(value);
|
|
18
|
+
if (editable.type === "multiselect") {
|
|
19
|
+
return /* @__PURE__ */ jsx(
|
|
20
|
+
ControlledSelect,
|
|
21
|
+
{
|
|
22
|
+
className: "w-full",
|
|
23
|
+
label,
|
|
24
|
+
options: Array.isArray(editable.options) ? editable.options : [],
|
|
25
|
+
selectedValue: arrayValue,
|
|
26
|
+
onChangeHook: (selected) => onChange(Array.isArray(selected) ? selected : [selected]),
|
|
27
|
+
multiple: true,
|
|
28
|
+
searchable: true,
|
|
29
|
+
clearable: true,
|
|
30
|
+
disabled
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (editable.type === "select") {
|
|
35
|
+
return /* @__PURE__ */ jsx(
|
|
36
|
+
ControlledSelect,
|
|
37
|
+
{
|
|
38
|
+
className: "w-full",
|
|
39
|
+
label,
|
|
40
|
+
options: Array.isArray(editable.options) ? editable.options : [],
|
|
41
|
+
selectedValue: stringValue,
|
|
42
|
+
onChangeHook: (selected) => onChange(typeof selected === "string" ? selected : ""),
|
|
43
|
+
multiple: false,
|
|
44
|
+
searchable: true,
|
|
45
|
+
clearable: true,
|
|
46
|
+
disabled
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (editable.type === "date") {
|
|
51
|
+
const parsed = stringValue ? new Date(stringValue) : void 0;
|
|
52
|
+
const startValue = parsed && !Number.isNaN(parsed.getTime()) ? parsed : void 0;
|
|
53
|
+
return /* @__PURE__ */ jsx(
|
|
54
|
+
DatePicker,
|
|
55
|
+
{
|
|
56
|
+
label,
|
|
57
|
+
startValue,
|
|
58
|
+
onUpdate: (nextValue) => onChange(nextValue ?? ""),
|
|
59
|
+
disabled
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return /* @__PURE__ */ jsx(
|
|
64
|
+
ControlledInput,
|
|
65
|
+
{
|
|
66
|
+
label,
|
|
67
|
+
value: stringValue,
|
|
68
|
+
onChange: (event) => onChange(event.target.value),
|
|
69
|
+
inputMode: editable.type === "number" ? "decimal" : void 0,
|
|
70
|
+
disabled,
|
|
71
|
+
error: !!errorMessage,
|
|
72
|
+
errorMessage: errorMessage ?? void 0
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
export {
|
|
77
|
+
DataTableBulkEditValueEditor as default
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=DataTableBulkEditValueEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataTableBulkEditValueEditor.js","sources":["../../../../src/components/UI/DataTable/DataTableBulkEditValueEditor.tsx"],"sourcesContent":["import DatePicker from '@/components/DateTime/DatePicker';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport { ControlledSelect } from '@/components/Form/ControlledSelect';\n\nimport type { IDataTableEditableConfig, IDataTableRow } from './types';\n\ninterface IDataTableBulkEditValueEditorProps<T extends IDataTableRow> {\n editable: IDataTableEditableConfig<T>;\n value: unknown;\n onChange: (value: unknown) => void;\n label?: string;\n disabled?: boolean;\n errorMessage?: string | null;\n}\n\n/**\n * A value editor derived from the column's `editable` descriptor, the same one\n * the spreadsheet cell opens. Used by the \"same value\" mode (one editor for the\n * whole scope) and by the per-item matrix (one editor per cell).\n */\nconst DataTableBulkEditValueEditor = <T extends IDataTableRow>({\n editable,\n value,\n onChange,\n label,\n disabled,\n errorMessage,\n}: IDataTableBulkEditValueEditorProps<T>) => {\n if (editable.renderEditor) {\n return <>{editable.renderEditor({ value, onChange, disabled, label })}</>;\n }\n\n const arrayValue = Array.isArray(value) ? value.map(String) : [];\n const stringValue = value == null || Array.isArray(value) ? '' : String(value);\n\n if (editable.type === 'multiselect') {\n return (\n <ControlledSelect\n className=\"w-full\"\n label={label}\n options={Array.isArray(editable.options) ? editable.options : []}\n selectedValue={arrayValue}\n onChangeHook={(selected) => onChange(Array.isArray(selected) ? selected : [selected])}\n multiple\n searchable\n clearable\n disabled={disabled}\n />\n );\n }\n\n if (editable.type === 'select') {\n return (\n <ControlledSelect\n className=\"w-full\"\n label={label}\n options={Array.isArray(editable.options) ? editable.options : []}\n selectedValue={stringValue}\n onChangeHook={(selected) => onChange(typeof selected === 'string' ? selected : '')}\n multiple={false}\n searchable\n clearable\n disabled={disabled}\n />\n );\n }\n\n if (editable.type === 'date') {\n const parsed = stringValue ? new Date(stringValue) : undefined;\n const startValue = parsed && !Number.isNaN(parsed.getTime()) ? parsed : undefined;\n\n return (\n <DatePicker\n label={label}\n startValue={startValue}\n onUpdate={(nextValue) => onChange(nextValue ?? '')}\n disabled={disabled}\n />\n );\n }\n\n return (\n <ControlledInput\n label={label}\n value={stringValue}\n onChange={(event) => onChange(event.target.value)}\n inputMode={editable.type === 'number' ? 'decimal' : undefined}\n disabled={disabled}\n error={!!errorMessage}\n errorMessage={errorMessage ?? undefined}\n />\n );\n};\n\nexport default DataTableBulkEditValueEditor;\n"],"names":[],"mappings":";;;;AAoBA,MAAM,+BAA+B,CAA0B;AAAA,EAC7D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAA6C;AAC3C,MAAI,SAAS,cAAc;AACzB,WAAO,oBAAA,UAAA,EAAG,mBAAS,aAAa,EAAE,OAAO,UAAU,UAAU,MAAA,CAAO,EAAA,CAAE;AAAA,EACxE;AAEA,QAAM,aAAa,MAAM,QAAQ,KAAK,IAAI,MAAM,IAAI,MAAM,IAAI,CAAA;AAC9D,QAAM,cAAc,SAAS,QAAQ,MAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,KAAK;AAE7E,MAAI,SAAS,SAAS,eAAe;AACnC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV;AAAA,QACA,SAAS,MAAM,QAAQ,SAAS,OAAO,IAAI,SAAS,UAAU,CAAA;AAAA,QAC9D,eAAe;AAAA,QACf,cAAc,CAAC,aAAa,SAAS,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,QAAQ,CAAC;AAAA,QACpF,UAAQ;AAAA,QACR,YAAU;AAAA,QACV,WAAS;AAAA,QACT;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AAEA,MAAI,SAAS,SAAS,UAAU;AAC9B,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV;AAAA,QACA,SAAS,MAAM,QAAQ,SAAS,OAAO,IAAI,SAAS,UAAU,CAAA;AAAA,QAC9D,eAAe;AAAA,QACf,cAAc,CAAC,aAAa,SAAS,OAAO,aAAa,WAAW,WAAW,EAAE;AAAA,QACjF,UAAU;AAAA,QACV,YAAU;AAAA,QACV,WAAS;AAAA,QACT;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AAEA,MAAI,SAAS,SAAS,QAAQ;AAC5B,UAAM,SAAS,cAAc,IAAI,KAAK,WAAW,IAAI;AACrD,UAAM,aAAa,UAAU,CAAC,OAAO,MAAM,OAAO,QAAA,CAAS,IAAI,SAAS;AAExE,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,UAAU,CAAC,cAAc,SAAS,aAAa,EAAE;AAAA,QACjD;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,MACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,MAChD,WAAW,SAAS,SAAS,WAAW,YAAY;AAAA,MACpD;AAAA,MACA,OAAO,CAAC,CAAC;AAAA,MACT,cAAc,gBAAgB;AAAA,IAAA;AAAA,EAAA;AAGpC;"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const isBulkValueEmpty: (value: unknown) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Compares a cell draft with the row's current value. Editors produce strings
|
|
4
|
+
* while the document may store numbers, so both sides are compared in textual
|
|
5
|
+
* form; empty (null, '', []) is a single state, so editing and reverting does
|
|
6
|
+
* not count as a change.
|
|
7
|
+
*/
|
|
8
|
+
export declare const areBulkValuesEqual: (left: unknown, right: unknown) => boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const isBulkValueEmpty = (value) => Array.isArray(value) ? value.length === 0 : value == null || value === "";
|
|
2
|
+
const areBulkValuesEqual = (left, right) => {
|
|
3
|
+
if (isBulkValueEmpty(left) && isBulkValueEmpty(right)) return true;
|
|
4
|
+
if (isBulkValueEmpty(left) !== isBulkValueEmpty(right)) return false;
|
|
5
|
+
const leftEntries = (Array.isArray(left) ? left : [left]).map(String);
|
|
6
|
+
const rightEntries = (Array.isArray(right) ? right : [right]).map(String);
|
|
7
|
+
return leftEntries.length === rightEntries.length && leftEntries.every((entry, index) => entry === rightEntries[index]);
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
areBulkValuesEqual,
|
|
11
|
+
isBulkValueEmpty
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=bulkEditValues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulkEditValues.js","sources":["../../../../src/components/UI/DataTable/bulkEditValues.ts"],"sourcesContent":["export const isBulkValueEmpty = (value: unknown): boolean =>\n Array.isArray(value) ? value.length === 0 : value == null || value === '';\n\n/**\n * Compares a cell draft with the row's current value. Editors produce strings\n * while the document may store numbers, so both sides are compared in textual\n * form; empty (null, '', []) is a single state, so editing and reverting does\n * not count as a change.\n */\nexport const areBulkValuesEqual = (left: unknown, right: unknown): boolean => {\n if (isBulkValueEmpty(left) && isBulkValueEmpty(right)) return true;\n if (isBulkValueEmpty(left) !== isBulkValueEmpty(right)) return false;\n\n const leftEntries = (Array.isArray(left) ? left : [left]).map(String);\n const rightEntries = (Array.isArray(right) ? right : [right]).map(String);\n\n return (\n leftEntries.length === rightEntries.length && leftEntries.every((entry, index) => entry === rightEntries[index])\n );\n};\n"],"names":[],"mappings":"AAAO,MAAM,mBAAmB,CAAC,UAC/B,MAAM,QAAQ,KAAK,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,UAAU;AAQlE,MAAM,qBAAqB,CAAC,MAAe,UAA4B;AAC5E,MAAI,iBAAiB,IAAI,KAAK,iBAAiB,KAAK,EAAG,QAAO;AAC9D,MAAI,iBAAiB,IAAI,MAAM,iBAAiB,KAAK,EAAG,QAAO;AAE/D,QAAM,eAAe,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,MAAM;AACpE,QAAM,gBAAgB,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG,IAAI,MAAM;AAExE,SACE,YAAY,WAAW,aAAa,UAAU,YAAY,MAAM,CAAC,OAAO,UAAU,UAAU,aAAa,KAAK,CAAC;AAEnH;"}
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import classNames from "classnames";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
3
4
|
import { sanitizeRichText } from "../../helpers/richText.js";
|
|
4
5
|
const RICH_TEXT_CONTENT_CLASS = classNames(
|
|
5
6
|
"break-words text-sm leading-relaxed",
|
|
6
7
|
"[&_a]:text-brand-emphasis [&_a]:underline",
|
|
7
8
|
"[&_a.rt-file]:text-foreground-muted [&_a.rt-file]:no-underline",
|
|
9
|
+
"[&_a.rt-image-link]:no-underline",
|
|
8
10
|
"[&_ol]:ml-4 [&_ol]:list-decimal [&_ul]:ml-4 [&_ul]:list-disc",
|
|
9
11
|
"[&_blockquote]:border-l-2 [&_blockquote]:border-brand-border [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-foreground-subtle",
|
|
10
12
|
"[&_user]:whitespace-nowrap [&_user]:rounded [&_user]:bg-brand-soft [&_user]:px-1 [&_user]:font-medium [&_user]:text-brand-emphasis"
|
|
11
13
|
);
|
|
12
14
|
const RichTextContent = ({ html, className, plainText = false }) => {
|
|
13
15
|
if (plainText) {
|
|
14
|
-
return /* @__PURE__ */ jsx("div", { className:
|
|
16
|
+
return /* @__PURE__ */ jsx("div", { className: twMerge(RICH_TEXT_CONTENT_CLASS, "whitespace-pre-line", className), children: html });
|
|
15
17
|
}
|
|
16
18
|
return /* @__PURE__ */ jsx(
|
|
17
19
|
"div",
|
|
18
20
|
{
|
|
19
|
-
className:
|
|
21
|
+
className: twMerge(RICH_TEXT_CONTENT_CLASS, className),
|
|
20
22
|
dangerouslySetInnerHTML: { __html: sanitizeRichText(html) }
|
|
21
23
|
}
|
|
22
24
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextContent.js","sources":["../../../src/components/UI/RichTextContent.tsx"],"sourcesContent":["import classNames from 'classnames';\n\nimport { sanitizeRichText } from '@/helpers/richText';\n\ninterface IRichTextContentProps {\n html?: string;\n className?: string;\n /**\n * Reads the value as legacy plain text: real line breaks, no markup. Opt in per surface with\n * `isPlainTextBody`, never by default: a body whose only markup is a `<user>` mention would\n * otherwise render its tags as visible text.\n */\n plainText?: boolean;\n}\n\n/** The typography of a rich text body, shared by every surface that renders one. */\nexport const RICH_TEXT_CONTENT_CLASS = classNames(\n 'break-words text-sm leading-relaxed',\n '[&_a]:text-brand-emphasis [&_a]:underline',\n '[&_a.rt-file]:text-foreground-muted [&_a.rt-file]:no-underline',\n '[&_ol]:ml-4 [&_ol]:list-decimal [&_ul]:ml-4 [&_ul]:list-disc',\n '[&_blockquote]:border-l-2 [&_blockquote]:border-brand-border [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-foreground-subtle',\n '[&_user]:whitespace-nowrap [&_user]:rounded [&_user]:bg-brand-soft [&_user]:px-1 [&_user]:font-medium [&_user]:text-brand-emphasis',\n);\n\nconst RichTextContent: React.FC<IRichTextContentProps> = ({ html, className, plainText = false }) => {\n if (plainText) {\n return <div className={
|
|
1
|
+
{"version":3,"file":"RichTextContent.js","sources":["../../../src/components/UI/RichTextContent.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { twMerge } from 'tailwind-merge';\n\nimport { sanitizeRichText } from '@/helpers/richText';\n\ninterface IRichTextContentProps {\n html?: string;\n className?: string;\n /**\n * Reads the value as legacy plain text: real line breaks, no markup. Opt in per surface with\n * `isPlainTextBody`, never by default: a body whose only markup is a `<user>` mention would\n * otherwise render its tags as visible text.\n */\n plainText?: boolean;\n}\n\n/** The typography of a rich text body, shared by every surface that renders one. */\nexport const RICH_TEXT_CONTENT_CLASS = classNames(\n 'break-words text-sm leading-relaxed',\n '[&_a]:text-brand-emphasis [&_a]:underline',\n '[&_a.rt-file]:text-foreground-muted [&_a.rt-file]:no-underline',\n '[&_a.rt-image-link]:no-underline',\n '[&_ol]:ml-4 [&_ol]:list-decimal [&_ul]:ml-4 [&_ul]:list-disc',\n '[&_blockquote]:border-l-2 [&_blockquote]:border-brand-border [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-foreground-subtle',\n '[&_user]:whitespace-nowrap [&_user]:rounded [&_user]:bg-brand-soft [&_user]:px-1 [&_user]:font-medium [&_user]:text-brand-emphasis',\n);\n\nconst RichTextContent: React.FC<IRichTextContentProps> = ({ html, className, plainText = false }) => {\n if (plainText) {\n return <div className={twMerge(RICH_TEXT_CONTENT_CLASS, 'whitespace-pre-line', className)}>{html}</div>;\n }\n\n return (\n <div\n className={twMerge(RICH_TEXT_CONTENT_CLASS, className)}\n dangerouslySetInnerHTML={{ __html: sanitizeRichText(html) }}\n />\n );\n};\n\nexport default RichTextContent;\n"],"names":[],"mappings":";;;;AAiBO,MAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,kBAAmD,CAAC,EAAE,MAAM,WAAW,YAAY,YAAY;AACnG,MAAI,WAAW;AACb,WAAO,oBAAC,SAAI,WAAW,QAAQ,yBAAyB,uBAAuB,SAAS,GAAI,UAAA,KAAA,CAAK;AAAA,EACnG;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW,QAAQ,yBAAyB,SAAS;AAAA,MACrD,yBAAyB,EAAE,QAAQ,iBAAiB,IAAI,EAAA;AAAA,IAAE;AAAA,EAAA;AAGhE;"}
|
|
@@ -5,6 +5,9 @@ export declare const RICH_TEXT_COLOR_CLASS: Record<Exclude<TRichTextColor, 'defa
|
|
|
5
5
|
/** The swatch shown in the palette: a token-driven background, never the stored value. */
|
|
6
6
|
export declare const RICH_TEXT_COLOR_SWATCH_CLASS: Record<TRichTextColor, string>;
|
|
7
7
|
export declare const RICH_TEXT_FILE_CLASS = "rt-file";
|
|
8
|
+
/** An image written INTO the body, as opposed to a file referenced by name. */
|
|
9
|
+
export declare const RICH_TEXT_IMAGE_CLASS = "rt-image";
|
|
10
|
+
export declare const RICH_TEXT_IMAGE_LINK_CLASS = "rt-image-link";
|
|
8
11
|
/**
|
|
9
12
|
* Text written before the rich editor existed is plain text with real line breaks. Rendered as
|
|
10
13
|
* HTML those breaks collapse, so the renderer needs to know which format it is looking at.
|
|
@@ -13,6 +16,10 @@ export declare const isPlainTextBody: (html?: string) => boolean;
|
|
|
13
16
|
export declare const sanitizeRichText: (html?: string) => string;
|
|
14
17
|
/** Emptiness has to be measured on the text: an editor the user only clicked into holds `<br>`. */
|
|
15
18
|
export declare const richTextToPlainText: (html?: string) => string;
|
|
19
|
+
/**
|
|
20
|
+
* A body whose only content is an attachment has no text at all, so measuring emptiness on the
|
|
21
|
+
* text alone would throw away the picture the user just wrote into the field.
|
|
22
|
+
*/
|
|
16
23
|
export declare const isRichTextEmpty: (html?: string) => boolean;
|
|
17
24
|
/**
|
|
18
25
|
* Seeds the editor from a value stored before the field became rich text. The line structure is
|
|
@@ -33,5 +40,7 @@ export declare const activeRichTextColor: (editor: HTMLElement) => TRichTextColo
|
|
|
33
40
|
/**
|
|
34
41
|
* An uploaded file referenced from inside the body. Hosts that have no attachment field of
|
|
35
42
|
* their own still keep the reference, because it travels in the same string the text does.
|
|
43
|
+
* An image is written as the picture itself, wrapped in the link that opens it at full size:
|
|
44
|
+
* a name-only chip hides what was attached, and the reader has no way to look at it.
|
|
36
45
|
*/
|
|
37
46
|
export declare const buildFileChipMarkup: (attachment: IFileAttachment) => string;
|
package/dist/helpers/richText.js
CHANGED
|
@@ -33,8 +33,15 @@ const RICH_TEXT_COLOR_SWATCH_CLASS = {
|
|
|
33
33
|
muted: "bg-foreground-faint"
|
|
34
34
|
};
|
|
35
35
|
const RICH_TEXT_FILE_CLASS = "rt-file";
|
|
36
|
+
const RICH_TEXT_IMAGE_CLASS = "rt-image";
|
|
37
|
+
const RICH_TEXT_IMAGE_LINK_CLASS = "rt-image-link";
|
|
36
38
|
const COLOR_CLASS_LIST = Object.values(RICH_TEXT_COLOR_CLASS);
|
|
37
|
-
const ALLOWED_CLASSES = /* @__PURE__ */ new Set([
|
|
39
|
+
const ALLOWED_CLASSES = /* @__PURE__ */ new Set([
|
|
40
|
+
...COLOR_CLASS_LIST,
|
|
41
|
+
RICH_TEXT_FILE_CLASS,
|
|
42
|
+
RICH_TEXT_IMAGE_CLASS,
|
|
43
|
+
RICH_TEXT_IMAGE_LINK_CLASS
|
|
44
|
+
]);
|
|
38
45
|
const ALLOWED_TAGS = [
|
|
39
46
|
MENTION_TAG,
|
|
40
47
|
"p",
|
|
@@ -52,9 +59,10 @@ const ALLOWED_TAGS = [
|
|
|
52
59
|
"ol",
|
|
53
60
|
"li",
|
|
54
61
|
"blockquote",
|
|
55
|
-
"a"
|
|
62
|
+
"a",
|
|
63
|
+
"img"
|
|
56
64
|
];
|
|
57
|
-
const ALLOWED_ATTR = ["href", "target", "rel", "class"];
|
|
65
|
+
const ALLOWED_ATTR = ["href", "target", "rel", "class", "src", "alt", "width", "height"];
|
|
58
66
|
let hookRegistered = false;
|
|
59
67
|
const registerSanitizerHooks = () => {
|
|
60
68
|
if (hookRegistered) return;
|
|
@@ -89,7 +97,11 @@ const richTextToPlainText = (html) => {
|
|
|
89
97
|
const parsed = new DOMParser().parseFromString(sanitizeRichText(html), "text/html");
|
|
90
98
|
return (parsed.body.textContent ?? "").trim();
|
|
91
99
|
};
|
|
92
|
-
const isRichTextEmpty = (html) =>
|
|
100
|
+
const isRichTextEmpty = (html) => {
|
|
101
|
+
if (richTextToPlainText(html).length > 0) return false;
|
|
102
|
+
const parsed = new DOMParser().parseFromString(sanitizeRichText(html), "text/html");
|
|
103
|
+
return parsed.body.querySelector(`img, .${RICH_TEXT_FILE_CLASS}`) === null;
|
|
104
|
+
};
|
|
93
105
|
const HTML_ESCAPES = {
|
|
94
106
|
"&": "&",
|
|
95
107
|
"<": "<",
|
|
@@ -143,20 +155,34 @@ const activeRichTextColor = (editor) => {
|
|
|
143
155
|
);
|
|
144
156
|
return match?.[0] ?? "default";
|
|
145
157
|
};
|
|
158
|
+
const isImageAttachment = (attachment) => attachment.kind === "image" || attachment.mimeType?.startsWith("image/") === true;
|
|
146
159
|
const buildFileChipMarkup = (attachment) => {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
const link = document.createElement("a");
|
|
161
|
+
link.setAttribute("href", attachment.url);
|
|
162
|
+
link.setAttribute("target", "_blank");
|
|
163
|
+
link.setAttribute("rel", "noopener noreferrer");
|
|
164
|
+
if (!isImageAttachment(attachment)) {
|
|
165
|
+
link.setAttribute("class", RICH_TEXT_FILE_CLASS);
|
|
166
|
+
link.textContent = attachment.name;
|
|
167
|
+
return link.outerHTML;
|
|
168
|
+
}
|
|
169
|
+
const image = document.createElement("img");
|
|
170
|
+
image.setAttribute("src", attachment.url);
|
|
171
|
+
image.setAttribute("alt", attachment.name);
|
|
172
|
+
image.setAttribute("class", RICH_TEXT_IMAGE_CLASS);
|
|
173
|
+
if (attachment.width) image.setAttribute("width", String(attachment.width));
|
|
174
|
+
if (attachment.height) image.setAttribute("height", String(attachment.height));
|
|
175
|
+
link.setAttribute("class", RICH_TEXT_IMAGE_LINK_CLASS);
|
|
176
|
+
link.appendChild(image);
|
|
177
|
+
return link.outerHTML;
|
|
154
178
|
};
|
|
155
179
|
export {
|
|
156
180
|
RICH_TEXT_COLORS,
|
|
157
181
|
RICH_TEXT_COLOR_CLASS,
|
|
158
182
|
RICH_TEXT_COLOR_SWATCH_CLASS,
|
|
159
183
|
RICH_TEXT_FILE_CLASS,
|
|
184
|
+
RICH_TEXT_IMAGE_CLASS,
|
|
185
|
+
RICH_TEXT_IMAGE_LINK_CLASS,
|
|
160
186
|
activeRichTextColor,
|
|
161
187
|
applyRichTextColor,
|
|
162
188
|
buildFileChipMarkup,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"richText.js","sources":["../../src/helpers/richText.ts"],"sourcesContent":["import DOMPurify from 'dompurify';\n\nimport { IFileAttachment } from '../interfaces/IFileAttachment';\n\n/**\n * Rich text authored in this app is HTML written by a user, so nothing here reaches the DOM\n * without passing through the sanitizer first, and nothing the editor writes may carry a\n * literal colour: a hex picked under the light theme is unreadable under the dark one. Colour\n * travels as one of these class names and the theme resolves it, so the same stored string\n * stays legible in both.\n */\nconst MENTION_TAG = 'user';\n\nexport type TRichTextColor =\n 'default' | 'brand' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'info' | 'muted';\n\nexport const RICH_TEXT_COLORS: Array<TRichTextColor> = [\n 'default',\n 'brand',\n 'info',\n 'success',\n 'warning',\n 'danger',\n 'accent',\n 'secondary',\n 'muted',\n];\n\nexport const RICH_TEXT_COLOR_CLASS: Record<Exclude<TRichTextColor, 'default'>, string> = {\n brand: 'rt-color-brand',\n secondary: 'rt-color-secondary',\n accent: 'rt-color-accent',\n success: 'rt-color-success',\n warning: 'rt-color-warning',\n danger: 'rt-color-danger',\n info: 'rt-color-info',\n muted: 'rt-color-muted',\n};\n\n/** The swatch shown in the palette: a token-driven background, never the stored value. */\nexport const RICH_TEXT_COLOR_SWATCH_CLASS: Record<TRichTextColor, string> = {\n default: 'bg-foreground',\n brand: 'bg-brand',\n secondary: 'bg-secondary',\n accent: 'bg-accent',\n success: 'bg-success',\n warning: 'bg-warning',\n danger: 'bg-danger',\n info: 'bg-info',\n muted: 'bg-foreground-faint',\n};\n\nexport const RICH_TEXT_FILE_CLASS = 'rt-file';\n\nconst COLOR_CLASS_LIST = Object.values(RICH_TEXT_COLOR_CLASS);\nconst ALLOWED_CLASSES = new Set<string>([...COLOR_CLASS_LIST, RICH_TEXT_FILE_CLASS]);\n\nconst ALLOWED_TAGS = [\n MENTION_TAG,\n 'p',\n 'br',\n 'div',\n 'span',\n 'b',\n 'strong',\n 'i',\n 'em',\n 's',\n 'strike',\n 'u',\n 'ul',\n 'ol',\n 'li',\n 'blockquote',\n 'a',\n];\n\nconst ALLOWED_ATTR = ['href', 'target', 'rel', 'class'];\n\nlet hookRegistered = false;\n\nconst registerSanitizerHooks = (): void => {\n if (hookRegistered) return;\n hookRegistered = true;\n\n DOMPurify.addHook('afterSanitizeAttributes', (node) => {\n const element = node as HTMLElement;\n\n // A link authored by someone else must not be able to reach back into this tab.\n if (element.tagName === 'A') {\n element.setAttribute('target', '_blank');\n element.setAttribute('rel', 'noopener noreferrer');\n }\n\n if (!element.getAttribute?.('class')) return;\n\n const kept = (element.getAttribute('class') ?? '')\n .split(/\\s+/)\n .filter((className) => ALLOWED_CLASSES.has(className));\n\n if (kept.length) element.setAttribute('class', kept.join(' '));\n else element.removeAttribute('class');\n });\n};\n\n/**\n * Text written before the rich editor existed is plain text with real line breaks. Rendered as\n * HTML those breaks collapse, so the renderer needs to know which format it is looking at.\n */\nexport const isPlainTextBody = (html?: string): boolean => {\n if (!html) return true;\n\n return !/<(?!\\/?user\\b)[a-z][^>]*>/i.test(html);\n};\n\nexport const sanitizeRichText = (html?: string): string => {\n if (!html) return '';\n\n registerSanitizerHooks();\n\n return DOMPurify.sanitize(html, {\n ALLOWED_TAGS,\n ALLOWED_ATTR,\n ALLOW_DATA_ATTR: false,\n });\n};\n\n/** Emptiness has to be measured on the text: an editor the user only clicked into holds `<br>`. */\nexport const richTextToPlainText = (html?: string): string => {\n if (!html) return '';\n\n const parsed = new DOMParser().parseFromString(sanitizeRichText(html), 'text/html');\n return (parsed.body.textContent ?? '').trim();\n};\n\nexport const isRichTextEmpty = (html?: string): boolean => richTextToPlainText(html).length === 0;\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n};\n\n/**\n * Seeds the editor from a value stored before the field became rich text. The line structure is\n * preserved and the text is escaped, so nothing in it is read as markup.\n */\nexport const plainTextToRichHtml = (text?: string): string => {\n if (!text) return '';\n\n return text.replace(/[&<>]/g, (character) => HTML_ESCAPES[character] ?? character).replace(/\\n/g, '<br>');\n};\n\n/** The value as the editor should receive it, whichever format it was stored in. */\nexport const toEditableRichHtml = (value?: string): string =>\n isPlainTextBody(value) ? plainTextToRichHtml(value) : (value ?? '');\n\n/**\n * `foreColor` is the only command that knows how to split a partial selection, so the colour is\n * applied through it with a value nothing else in the app uses, and the nodes it just produced\n * are then rewritten to the theme class. Nothing keeps the literal colour.\n */\nconst COLOR_SENTINEL_HEX = '#010203';\nconst COLOR_SENTINEL_RGB = 'rgb(1,2,3)';\n\nconst stripColorClasses = (element: HTMLElement): void => {\n COLOR_CLASS_LIST.forEach((className) => element.classList.remove(className));\n if (!element.classList.length) element.removeAttribute('class');\n};\n\nconst unwrapIfBare = (element: HTMLElement): void => {\n if (element.tagName !== 'SPAN' || element.attributes.length > 0) return;\n\n const parent = element.parentNode;\n if (!parent) return;\n\n while (element.firstChild) parent.insertBefore(element.firstChild, element);\n parent.removeChild(element);\n};\n\n/**\n * Rewrites whatever `foreColor` just produced. Called again on every input because a colour\n * chosen with no selection only materialises on the next typed character: without this pass\n * that character would keep the literal sentinel colour, which the sanitizer drops.\n */\nexport const normalizeRichTextColorNodes = (editor: HTMLElement, color: TRichTextColor): void => {\n const targetClass = color === 'default' ? '' : RICH_TEXT_COLOR_CLASS[color];\n\n editor.querySelectorAll<HTMLElement>('[style*=\"color\"]').forEach((node) => {\n if (node.style.color.replace(/\\s/g, '') !== COLOR_SENTINEL_RGB) return;\n\n node.style.removeProperty('color');\n if (!node.getAttribute('style')) node.removeAttribute('style');\n\n node.querySelectorAll<HTMLElement>('[class]').forEach(stripColorClasses);\n stripColorClasses(node);\n\n if (targetClass) node.classList.add(targetClass);\n else unwrapIfBare(node);\n });\n};\n\nexport const applyRichTextColor = (editor: HTMLElement, color: TRichTextColor): void => {\n document.execCommand('styleWithCSS', false, 'true');\n document.execCommand('foreColor', false, COLOR_SENTINEL_HEX);\n normalizeRichTextColorNodes(editor, color);\n};\n\n/** The colour of the text under the caret, so the palette can show what is currently applied. */\nexport const activeRichTextColor = (editor: HTMLElement): TRichTextColor => {\n const selection = window.getSelection();\n const anchor = selection?.anchorNode;\n if (!anchor) return 'default';\n\n const element = anchor.nodeType === Node.ELEMENT_NODE ? (anchor as HTMLElement) : anchor.parentElement;\n if (!element || !editor.contains(element)) return 'default';\n\n const colored = element.closest<HTMLElement>(COLOR_CLASS_LIST.map((className) => `.${className}`).join(','));\n if (!colored || !editor.contains(colored)) return 'default';\n\n const match = (Object.entries(RICH_TEXT_COLOR_CLASS) as Array<[Exclude<TRichTextColor, 'default'>, string]>).find(\n ([, className]) => colored.classList.contains(className),\n );\n\n return match?.[0] ?? 'default';\n};\n\n/**\n * An uploaded file referenced from inside the body. Hosts that have no attachment field of\n * their own still keep the reference, because it travels in the same string the text does.\n */\nexport const buildFileChipMarkup = (attachment: IFileAttachment): string => {\n const chip = document.createElement('a');\n chip.setAttribute('href', attachment.url);\n chip.setAttribute('class', RICH_TEXT_FILE_CLASS);\n chip.setAttribute('target', '_blank');\n chip.setAttribute('rel', 'noopener noreferrer');\n chip.textContent = attachment.name;\n return chip.outerHTML;\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"richText.js","sources":["../../src/helpers/richText.ts"],"sourcesContent":["import DOMPurify from 'dompurify';\n\nimport { IFileAttachment } from '../interfaces/IFileAttachment';\n\n/**\n * Rich text authored in this app is HTML written by a user, so nothing here reaches the DOM\n * without passing through the sanitizer first, and nothing the editor writes may carry a\n * literal colour: a hex picked under the light theme is unreadable under the dark one. Colour\n * travels as one of these class names and the theme resolves it, so the same stored string\n * stays legible in both.\n */\nconst MENTION_TAG = 'user';\n\nexport type TRichTextColor =\n 'default' | 'brand' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'info' | 'muted';\n\nexport const RICH_TEXT_COLORS: Array<TRichTextColor> = [\n 'default',\n 'brand',\n 'info',\n 'success',\n 'warning',\n 'danger',\n 'accent',\n 'secondary',\n 'muted',\n];\n\nexport const RICH_TEXT_COLOR_CLASS: Record<Exclude<TRichTextColor, 'default'>, string> = {\n brand: 'rt-color-brand',\n secondary: 'rt-color-secondary',\n accent: 'rt-color-accent',\n success: 'rt-color-success',\n warning: 'rt-color-warning',\n danger: 'rt-color-danger',\n info: 'rt-color-info',\n muted: 'rt-color-muted',\n};\n\n/** The swatch shown in the palette: a token-driven background, never the stored value. */\nexport const RICH_TEXT_COLOR_SWATCH_CLASS: Record<TRichTextColor, string> = {\n default: 'bg-foreground',\n brand: 'bg-brand',\n secondary: 'bg-secondary',\n accent: 'bg-accent',\n success: 'bg-success',\n warning: 'bg-warning',\n danger: 'bg-danger',\n info: 'bg-info',\n muted: 'bg-foreground-faint',\n};\n\nexport const RICH_TEXT_FILE_CLASS = 'rt-file';\n/** An image written INTO the body, as opposed to a file referenced by name. */\nexport const RICH_TEXT_IMAGE_CLASS = 'rt-image';\nexport const RICH_TEXT_IMAGE_LINK_CLASS = 'rt-image-link';\n\nconst COLOR_CLASS_LIST = Object.values(RICH_TEXT_COLOR_CLASS);\nconst ALLOWED_CLASSES = new Set<string>([\n ...COLOR_CLASS_LIST,\n RICH_TEXT_FILE_CLASS,\n RICH_TEXT_IMAGE_CLASS,\n RICH_TEXT_IMAGE_LINK_CLASS,\n]);\n\nconst ALLOWED_TAGS = [\n MENTION_TAG,\n 'p',\n 'br',\n 'div',\n 'span',\n 'b',\n 'strong',\n 'i',\n 'em',\n 's',\n 'strike',\n 'u',\n 'ul',\n 'ol',\n 'li',\n 'blockquote',\n 'a',\n 'img',\n];\n\nconst ALLOWED_ATTR = ['href', 'target', 'rel', 'class', 'src', 'alt', 'width', 'height'];\n\nlet hookRegistered = false;\n\nconst registerSanitizerHooks = (): void => {\n if (hookRegistered) return;\n hookRegistered = true;\n\n DOMPurify.addHook('afterSanitizeAttributes', (node) => {\n const element = node as HTMLElement;\n\n // A link authored by someone else must not be able to reach back into this tab.\n if (element.tagName === 'A') {\n element.setAttribute('target', '_blank');\n element.setAttribute('rel', 'noopener noreferrer');\n }\n\n if (!element.getAttribute?.('class')) return;\n\n const kept = (element.getAttribute('class') ?? '')\n .split(/\\s+/)\n .filter((className) => ALLOWED_CLASSES.has(className));\n\n if (kept.length) element.setAttribute('class', kept.join(' '));\n else element.removeAttribute('class');\n });\n};\n\n/**\n * Text written before the rich editor existed is plain text with real line breaks. Rendered as\n * HTML those breaks collapse, so the renderer needs to know which format it is looking at.\n */\nexport const isPlainTextBody = (html?: string): boolean => {\n if (!html) return true;\n\n return !/<(?!\\/?user\\b)[a-z][^>]*>/i.test(html);\n};\n\nexport const sanitizeRichText = (html?: string): string => {\n if (!html) return '';\n\n registerSanitizerHooks();\n\n return DOMPurify.sanitize(html, {\n ALLOWED_TAGS,\n ALLOWED_ATTR,\n ALLOW_DATA_ATTR: false,\n });\n};\n\n/** Emptiness has to be measured on the text: an editor the user only clicked into holds `<br>`. */\nexport const richTextToPlainText = (html?: string): string => {\n if (!html) return '';\n\n const parsed = new DOMParser().parseFromString(sanitizeRichText(html), 'text/html');\n return (parsed.body.textContent ?? '').trim();\n};\n\n/**\n * A body whose only content is an attachment has no text at all, so measuring emptiness on the\n * text alone would throw away the picture the user just wrote into the field.\n */\nexport const isRichTextEmpty = (html?: string): boolean => {\n if (richTextToPlainText(html).length > 0) return false;\n\n const parsed = new DOMParser().parseFromString(sanitizeRichText(html), 'text/html');\n return parsed.body.querySelector(`img, .${RICH_TEXT_FILE_CLASS}`) === null;\n};\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n};\n\n/**\n * Seeds the editor from a value stored before the field became rich text. The line structure is\n * preserved and the text is escaped, so nothing in it is read as markup.\n */\nexport const plainTextToRichHtml = (text?: string): string => {\n if (!text) return '';\n\n return text.replace(/[&<>]/g, (character) => HTML_ESCAPES[character] ?? character).replace(/\\n/g, '<br>');\n};\n\n/** The value as the editor should receive it, whichever format it was stored in. */\nexport const toEditableRichHtml = (value?: string): string =>\n isPlainTextBody(value) ? plainTextToRichHtml(value) : (value ?? '');\n\n/**\n * `foreColor` is the only command that knows how to split a partial selection, so the colour is\n * applied through it with a value nothing else in the app uses, and the nodes it just produced\n * are then rewritten to the theme class. Nothing keeps the literal colour.\n */\nconst COLOR_SENTINEL_HEX = '#010203';\nconst COLOR_SENTINEL_RGB = 'rgb(1,2,3)';\n\nconst stripColorClasses = (element: HTMLElement): void => {\n COLOR_CLASS_LIST.forEach((className) => element.classList.remove(className));\n if (!element.classList.length) element.removeAttribute('class');\n};\n\nconst unwrapIfBare = (element: HTMLElement): void => {\n if (element.tagName !== 'SPAN' || element.attributes.length > 0) return;\n\n const parent = element.parentNode;\n if (!parent) return;\n\n while (element.firstChild) parent.insertBefore(element.firstChild, element);\n parent.removeChild(element);\n};\n\n/**\n * Rewrites whatever `foreColor` just produced. Called again on every input because a colour\n * chosen with no selection only materialises on the next typed character: without this pass\n * that character would keep the literal sentinel colour, which the sanitizer drops.\n */\nexport const normalizeRichTextColorNodes = (editor: HTMLElement, color: TRichTextColor): void => {\n const targetClass = color === 'default' ? '' : RICH_TEXT_COLOR_CLASS[color];\n\n editor.querySelectorAll<HTMLElement>('[style*=\"color\"]').forEach((node) => {\n if (node.style.color.replace(/\\s/g, '') !== COLOR_SENTINEL_RGB) return;\n\n node.style.removeProperty('color');\n if (!node.getAttribute('style')) node.removeAttribute('style');\n\n node.querySelectorAll<HTMLElement>('[class]').forEach(stripColorClasses);\n stripColorClasses(node);\n\n if (targetClass) node.classList.add(targetClass);\n else unwrapIfBare(node);\n });\n};\n\nexport const applyRichTextColor = (editor: HTMLElement, color: TRichTextColor): void => {\n document.execCommand('styleWithCSS', false, 'true');\n document.execCommand('foreColor', false, COLOR_SENTINEL_HEX);\n normalizeRichTextColorNodes(editor, color);\n};\n\n/** The colour of the text under the caret, so the palette can show what is currently applied. */\nexport const activeRichTextColor = (editor: HTMLElement): TRichTextColor => {\n const selection = window.getSelection();\n const anchor = selection?.anchorNode;\n if (!anchor) return 'default';\n\n const element = anchor.nodeType === Node.ELEMENT_NODE ? (anchor as HTMLElement) : anchor.parentElement;\n if (!element || !editor.contains(element)) return 'default';\n\n const colored = element.closest<HTMLElement>(COLOR_CLASS_LIST.map((className) => `.${className}`).join(','));\n if (!colored || !editor.contains(colored)) return 'default';\n\n const match = (Object.entries(RICH_TEXT_COLOR_CLASS) as Array<[Exclude<TRichTextColor, 'default'>, string]>).find(\n ([, className]) => colored.classList.contains(className),\n );\n\n return match?.[0] ?? 'default';\n};\n\nconst isImageAttachment = (attachment: IFileAttachment): boolean =>\n attachment.kind === 'image' || attachment.mimeType?.startsWith('image/') === true;\n\n/**\n * An uploaded file referenced from inside the body. Hosts that have no attachment field of\n * their own still keep the reference, because it travels in the same string the text does.\n * An image is written as the picture itself, wrapped in the link that opens it at full size:\n * a name-only chip hides what was attached, and the reader has no way to look at it.\n */\nexport const buildFileChipMarkup = (attachment: IFileAttachment): string => {\n const link = document.createElement('a');\n link.setAttribute('href', attachment.url);\n link.setAttribute('target', '_blank');\n link.setAttribute('rel', 'noopener noreferrer');\n\n if (!isImageAttachment(attachment)) {\n link.setAttribute('class', RICH_TEXT_FILE_CLASS);\n link.textContent = attachment.name;\n return link.outerHTML;\n }\n\n const image = document.createElement('img');\n image.setAttribute('src', attachment.url);\n image.setAttribute('alt', attachment.name);\n image.setAttribute('class', RICH_TEXT_IMAGE_CLASS);\n if (attachment.width) image.setAttribute('width', String(attachment.width));\n if (attachment.height) image.setAttribute('height', String(attachment.height));\n\n link.setAttribute('class', RICH_TEXT_IMAGE_LINK_CLASS);\n link.appendChild(image);\n return link.outerHTML;\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -153,7 +153,19 @@ declare const _default: {
|
|
|
153
153
|
"selectAll_other": "Select the {{count}} visible",
|
|
154
154
|
"confirm_one": "Apply to {{count}} item",
|
|
155
155
|
"confirm_other": "Apply to {{count}} items",
|
|
156
|
-
"confirmEmpty": "Pick the items"
|
|
156
|
+
"confirmEmpty": "Pick the items",
|
|
157
|
+
"subtitlePerItem": "Pick the fields and adjust each item's value; only what changed will be saved.",
|
|
158
|
+
"modeLabel": "How to apply",
|
|
159
|
+
"modeSame": "Same value",
|
|
160
|
+
"modePerItem": "Values per item",
|
|
161
|
+
"fields": "Fields",
|
|
162
|
+
"confirmChanges_one": "Apply {{count}} change",
|
|
163
|
+
"confirmChanges_other": "Apply {{count}} changes",
|
|
164
|
+
"confirmNoChanges": "No changes",
|
|
165
|
+
"pickFieldsHint": "Pick at least one field to edit per item.",
|
|
166
|
+
"noTargets": "No item in the chosen scope.",
|
|
167
|
+
"perItemHint": "Change only the cells you want. The rest stay as they are.",
|
|
168
|
+
"itemColumn": "Item"
|
|
157
169
|
}
|
|
158
170
|
},
|
|
159
171
|
"dateRangePicker": {
|
|
@@ -5,7 +5,7 @@ const controlledDrawer = { "resizeHandle": "Drag to adjust the width" };
|
|
|
5
5
|
const controlledSelect = { "loadingOptions": "Loading options", "noDataToShow": "No data to show", "moreItems": "+{{count}}" };
|
|
6
6
|
const controlledTabs = { "renameTab": "Rename tab", "tabDisabled": "This tab is disabled", "enableTab": "Enable tab", "mustKeepOneEnabled": "At least one tab must remain enabled", "disableTab": "Disable tab", "enableTabShort": "Enable tab" };
|
|
7
7
|
const createTask = { "newTask": "New task", "createTask": "Create Task", "newTaskTitle": "New Task", "taskName": "Title", "createdSuccess": "Task created successfully for {{date}}", "taskNamePlaceholder": "What is the task?", "taskType": "Task Type", "deadline": "Due date", "description": "Description", "descriptionPlaceholder": "Add a description...", "assignees": "Assignees", "saveTask": "Save Task", "cancel": "Cancel", "createdSuccessNoDate": "Task created successfully", "titleRequired": "Title is required", "taskTypeRequired": "Task type is required", "sidebarTitle": "Details", "rich": { "bold": "Bold", "italic": "Italic", "strikethrough": "Strikethrough", "link": "Link", "bulletList": "Bullet list", "numberedList": "Numbered list", "quote": "Quote", "indent": "Increase indent", "outdent": "Decrease indent", "textColor": "Text colour", "colors": { "default": "Default", "brand": "Purple", "info": "Blue", "success": "Green", "warning": "Amber", "danger": "Red", "accent": "Pink", "secondary": "Teal", "muted": "Grey" } }, "newTaskSubtitle": "Set the assignee and the due date", "createError": "Could not create the task", "requiredHint": "Fill in the title and the task type to save", "deadlinePlaceholder": "Select a date" };
|
|
8
|
-
const dataTable = { "empty": "No records found", "error": "We could not load this list", "columns": "Columns", "viewList": "List", "viewCards": "Cards", "reorderColumn": "Reorder column", "restoreDefaultOrder": "Restore default order", "resizeColumn": "Resize column", "restoreDefaultWidths": "Restore default widths", "viewGrouped": "Grouping", "viewKanban": "Kanban", "addRow": "Add row", "removeRow": "Remove row", "editCell": "Edit", "save": "Save", "fillHandle": "Drag to fill", "editMode": { "enter": "Spreadsheet editing", "exit": "Exit editing", "inactiveHint": "Turns on cell editing: keyboard navigation, selection and drag to fill", "activeHint": "Cell editing is on. Clicking a row opens the record again once you exit" }, "bulkEdit": { "title": "Bulk edit", "subtitle": "Pick the field, the new value and which items it applies to.", "open": "Bulk edit", "hint": "Applies the same value to several items at once", "field": "Field", "newValue": "New value", "scopeLabel": "Apply to", "scopeLoaded": "All loaded", "scopePicked": "Pick items", "searchRows": "Search the list", "noRowsFound": "No item matches the search.", "selectAll_one": "Select the visible item", "selectAll_other": "Select the {{count}} visible", "confirm_one": "Apply to {{count}} item", "confirm_other": "Apply to {{count}} items", "confirmEmpty": "Pick the items" } };
|
|
8
|
+
const dataTable = { "empty": "No records found", "error": "We could not load this list", "columns": "Columns", "viewList": "List", "viewCards": "Cards", "reorderColumn": "Reorder column", "restoreDefaultOrder": "Restore default order", "resizeColumn": "Resize column", "restoreDefaultWidths": "Restore default widths", "viewGrouped": "Grouping", "viewKanban": "Kanban", "addRow": "Add row", "removeRow": "Remove row", "editCell": "Edit", "save": "Save", "fillHandle": "Drag to fill", "editMode": { "enter": "Spreadsheet editing", "exit": "Exit editing", "inactiveHint": "Turns on cell editing: keyboard navigation, selection and drag to fill", "activeHint": "Cell editing is on. Clicking a row opens the record again once you exit" }, "bulkEdit": { "title": "Bulk edit", "subtitle": "Pick the field, the new value and which items it applies to.", "open": "Bulk edit", "hint": "Applies the same value to several items at once", "field": "Field", "newValue": "New value", "scopeLabel": "Apply to", "scopeLoaded": "All loaded", "scopePicked": "Pick items", "searchRows": "Search the list", "noRowsFound": "No item matches the search.", "selectAll_one": "Select the visible item", "selectAll_other": "Select the {{count}} visible", "confirm_one": "Apply to {{count}} item", "confirm_other": "Apply to {{count}} items", "confirmEmpty": "Pick the items", "subtitlePerItem": "Pick the fields and adjust each item's value; only what changed will be saved.", "modeLabel": "How to apply", "modeSame": "Same value", "modePerItem": "Values per item", "fields": "Fields", "confirmChanges_one": "Apply {{count}} change", "confirmChanges_other": "Apply {{count}} changes", "confirmNoChanges": "No changes", "pickFieldsHint": "Pick at least one field to edit per item.", "noTargets": "No item in the chosen scope.", "perItemHint": "Change only the cells you want. The rest stay as they are.", "itemColumn": "Item" } };
|
|
9
9
|
const dateRangePicker = { "selectPeriod": "Select a period", "nowSelectEndDate": "Now select the end date" };
|
|
10
10
|
const dates = { "today": "Today", "yesterday": "Yesterday", "tomorrow": "Tomorrow", "thisWeek": "This week", "lastWeek": "Last week", "thisMonth": "This month", "lastMonth": "Last month", "selectStartDate": "Select start date", "selectEndDate": "Select end date", "clearSelection": "Clear selection", "clearPeriod": "Clear selected period", "periodSelectedSuccess": "Period selected successfully" };
|
|
11
11
|
const detailsTable = { "size": "Size", "quantity": "Qty" };
|
package/dist/locales/index.d.ts
CHANGED
|
@@ -156,6 +156,18 @@ export declare const uiLocales: {
|
|
|
156
156
|
confirm_one: string;
|
|
157
157
|
confirm_other: string;
|
|
158
158
|
confirmEmpty: string;
|
|
159
|
+
subtitlePerItem: string;
|
|
160
|
+
modeLabel: string;
|
|
161
|
+
modeSame: string;
|
|
162
|
+
modePerItem: string;
|
|
163
|
+
fields: string;
|
|
164
|
+
confirmChanges_one: string;
|
|
165
|
+
confirmChanges_other: string;
|
|
166
|
+
confirmNoChanges: string;
|
|
167
|
+
pickFieldsHint: string;
|
|
168
|
+
noTargets: string;
|
|
169
|
+
perItemHint: string;
|
|
170
|
+
itemColumn: string;
|
|
159
171
|
};
|
|
160
172
|
};
|
|
161
173
|
dateRangePicker: {
|
|
@@ -651,6 +663,18 @@ export declare const uiLocales: {
|
|
|
651
663
|
confirm_one: string;
|
|
652
664
|
confirm_other: string;
|
|
653
665
|
confirmEmpty: string;
|
|
666
|
+
subtitlePerItem: string;
|
|
667
|
+
modeLabel: string;
|
|
668
|
+
modeSame: string;
|
|
669
|
+
modePerItem: string;
|
|
670
|
+
fields: string;
|
|
671
|
+
confirmChanges_one: string;
|
|
672
|
+
confirmChanges_other: string;
|
|
673
|
+
confirmNoChanges: string;
|
|
674
|
+
pickFieldsHint: string;
|
|
675
|
+
noTargets: string;
|
|
676
|
+
perItemHint: string;
|
|
677
|
+
itemColumn: string;
|
|
654
678
|
};
|
|
655
679
|
};
|
|
656
680
|
dateRangePicker: {
|
|
@@ -153,7 +153,19 @@ declare const _default: {
|
|
|
153
153
|
"selectAll_other": "Selecionar os {{count}} visíveis",
|
|
154
154
|
"confirm_one": "Aplicar a {{count}} item",
|
|
155
155
|
"confirm_other": "Aplicar a {{count}} itens",
|
|
156
|
-
"confirmEmpty": "Selecione os itens"
|
|
156
|
+
"confirmEmpty": "Selecione os itens",
|
|
157
|
+
"subtitlePerItem": "Escolha os campos e ajuste o valor de cada item; só o que mudou será salvo.",
|
|
158
|
+
"modeLabel": "Como aplicar",
|
|
159
|
+
"modeSame": "Mesmo valor",
|
|
160
|
+
"modePerItem": "Valores por item",
|
|
161
|
+
"fields": "Campos",
|
|
162
|
+
"confirmChanges_one": "Aplicar {{count}} alteração",
|
|
163
|
+
"confirmChanges_other": "Aplicar {{count}} alterações",
|
|
164
|
+
"confirmNoChanges": "Nenhuma alteração",
|
|
165
|
+
"pickFieldsHint": "Selecione ao menos um campo para editar por item.",
|
|
166
|
+
"noTargets": "Nenhum item no alcance escolhido.",
|
|
167
|
+
"perItemHint": "Altere apenas as células desejadas. As demais permanecem como estão.",
|
|
168
|
+
"itemColumn": "Item"
|
|
157
169
|
}
|
|
158
170
|
},
|
|
159
171
|
"dateRangePicker": {
|
|
@@ -5,7 +5,7 @@ const controlledDrawer = { "resizeHandle": "Arraste para ajustar a largura" };
|
|
|
5
5
|
const controlledSelect = { "loadingOptions": "Carregando opções", "noDataToShow": "Não há dados para mostrar", "moreItems": "+{{count}}" };
|
|
6
6
|
const controlledTabs = { "renameTab": "Renomear aba", "tabDisabled": "Esta aba está desabilitada", "enableTab": "Habilitar aba", "mustKeepOneEnabled": "É necessário manter ao menos uma aba habilitada", "disableTab": "Desabilitar tab", "enableTabShort": "Habilitar tab" };
|
|
7
7
|
const createTask = { "newTask": "Nova tarefa", "createTask": "Criar Tarefa", "newTaskTitle": "Nova Tarefa", "taskName": "Título", "createdSuccess": "Tarefa criada com sucesso para {{date}}", "taskNamePlaceholder": "Qual é a tarefa?", "taskType": "Tipo de Tarefa", "deadline": "Prazo", "description": "Descrição", "descriptionPlaceholder": "Adicione uma descrição...", "assignees": "Responsáveis", "saveTask": "Gravar Tarefa", "cancel": "Cancelar", "createdSuccessNoDate": "Tarefa criada com sucesso", "titleRequired": "O título é obrigatório", "taskTypeRequired": "O tipo de tarefa é obrigatório", "sidebarTitle": "Detalhes", "rich": { "bold": "Negrito", "italic": "Itálico", "strikethrough": "Tachado", "link": "Link", "bulletList": "Lista com marcador", "numberedList": "Lista numerada", "quote": "Citação", "indent": "Aumentar recuo", "outdent": "Diminuir recuo", "textColor": "Cor do texto", "colors": { "default": "Padrão", "brand": "Roxo", "info": "Azul", "success": "Verde", "warning": "Âmbar", "danger": "Vermelho", "accent": "Rosa", "secondary": "Turquesa", "muted": "Cinza" } }, "newTaskSubtitle": "Defina o responsável e o prazo", "createError": "Não foi possível criar a tarefa", "requiredHint": "Informe o título e o tipo de tarefa para gravar", "deadlinePlaceholder": "Selecionar data" };
|
|
8
|
-
const dataTable = { "empty": "Nenhum registro encontrado", "error": "Não conseguimos carregar esta lista", "columns": "Colunas", "viewList": "Lista", "viewCards": "Cards", "reorderColumn": "Reordenar coluna", "restoreDefaultOrder": "Restaurar ordem padrão", "resizeColumn": "Redimensionar coluna", "restoreDefaultWidths": "Restaurar largura padrão", "viewGrouped": "Agrupamento", "viewKanban": "Kanban", "addRow": "Adicionar linha", "removeRow": "Remover linha", "editCell": "Editar", "save": "Salvar", "fillHandle": "Arraste para preencher", "editMode": { "enter": "Editar em planilha", "exit": "Sair da edição", "inactiveHint": "Ativa a edição em célula: navegação por teclado, seleção e preenchimento por arraste", "activeHint": "Edição em célula ativa. O clique na linha volta a abrir o registro quando você sair" }, "bulkEdit": { "title": "Editar em massa", "subtitle": "Escolha o campo, o novo valor e a quais itens aplicar.", "open": "Editar em massa", "hint": "Aplica um mesmo valor a vários itens de uma vez", "field": "Campo", "newValue": "Novo valor", "scopeLabel": "Aplicar a", "scopeLoaded": "Todos os carregados", "scopePicked": "Escolher itens", "searchRows": "Buscar na lista", "noRowsFound": "Nenhum item corresponde à busca.", "selectAll_one": "Selecionar o item visível", "selectAll_other": "Selecionar os {{count}} visíveis", "confirm_one": "Aplicar a {{count}} item", "confirm_other": "Aplicar a {{count}} itens", "confirmEmpty": "Selecione os itens" } };
|
|
8
|
+
const dataTable = { "empty": "Nenhum registro encontrado", "error": "Não conseguimos carregar esta lista", "columns": "Colunas", "viewList": "Lista", "viewCards": "Cards", "reorderColumn": "Reordenar coluna", "restoreDefaultOrder": "Restaurar ordem padrão", "resizeColumn": "Redimensionar coluna", "restoreDefaultWidths": "Restaurar largura padrão", "viewGrouped": "Agrupamento", "viewKanban": "Kanban", "addRow": "Adicionar linha", "removeRow": "Remover linha", "editCell": "Editar", "save": "Salvar", "fillHandle": "Arraste para preencher", "editMode": { "enter": "Editar em planilha", "exit": "Sair da edição", "inactiveHint": "Ativa a edição em célula: navegação por teclado, seleção e preenchimento por arraste", "activeHint": "Edição em célula ativa. O clique na linha volta a abrir o registro quando você sair" }, "bulkEdit": { "title": "Editar em massa", "subtitle": "Escolha o campo, o novo valor e a quais itens aplicar.", "open": "Editar em massa", "hint": "Aplica um mesmo valor a vários itens de uma vez", "field": "Campo", "newValue": "Novo valor", "scopeLabel": "Aplicar a", "scopeLoaded": "Todos os carregados", "scopePicked": "Escolher itens", "searchRows": "Buscar na lista", "noRowsFound": "Nenhum item corresponde à busca.", "selectAll_one": "Selecionar o item visível", "selectAll_other": "Selecionar os {{count}} visíveis", "confirm_one": "Aplicar a {{count}} item", "confirm_other": "Aplicar a {{count}} itens", "confirmEmpty": "Selecione os itens", "subtitlePerItem": "Escolha os campos e ajuste o valor de cada item; só o que mudou será salvo.", "modeLabel": "Como aplicar", "modeSame": "Mesmo valor", "modePerItem": "Valores por item", "fields": "Campos", "confirmChanges_one": "Aplicar {{count}} alteração", "confirmChanges_other": "Aplicar {{count}} alterações", "confirmNoChanges": "Nenhuma alteração", "pickFieldsHint": "Selecione ao menos um campo para editar por item.", "noTargets": "Nenhum item no alcance escolhido.", "perItemHint": "Altere apenas as células desejadas. As demais permanecem como estão.", "itemColumn": "Item" } };
|
|
9
9
|
const dateRangePicker = { "selectPeriod": "Selecione um período", "nowSelectEndDate": "Agora selecione a data final" };
|
|
10
10
|
const dates = { "today": "Hoje", "yesterday": "Ontem", "tomorrow": "Amanhã", "thisWeek": "Esta semana", "lastWeek": "Semana passada", "thisMonth": "Este mês", "lastMonth": "Mês passado", "selectStartDate": "Selecione a data inicial", "selectEndDate": "Selecione a data final", "clearSelection": "Limpar Seleção", "clearPeriod": "Limpar Período Selecionado", "periodSelectedSuccess": "Período selecionado com sucesso" };
|
|
11
11
|
const detailsTable = { "size": "Tam", "quantity": "Qtd" };
|
package/package.json
CHANGED
|
@@ -437,7 +437,7 @@ textarea:focus {
|
|
|
437
437
|
so the same message stays legible after a theme switch. Both the editor and every surface
|
|
438
438
|
that renders the sanitized body share these rules. */
|
|
439
439
|
.rt-color-brand {
|
|
440
|
-
color: rgb(var(--color-
|
|
440
|
+
color: rgb(var(--color-primary-emphasis));
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
.rt-color-secondary {
|
|
@@ -468,6 +468,28 @@ textarea:focus {
|
|
|
468
468
|
color: rgb(var(--color-foreground-muted));
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
+
.rt-image-link {
|
|
472
|
+
display: inline-block;
|
|
473
|
+
max-width: 100%;
|
|
474
|
+
line-height: 0;
|
|
475
|
+
text-decoration: none;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* An image written into the body shows itself: a name-only chip hides what was attached.
|
|
479
|
+
The cap keeps a full-resolution photo from taking over the field it was written into. */
|
|
480
|
+
.rt-image {
|
|
481
|
+
display: inline-block;
|
|
482
|
+
max-width: 100%;
|
|
483
|
+
max-height: 16rem;
|
|
484
|
+
width: auto;
|
|
485
|
+
height: auto;
|
|
486
|
+
border: 1px solid rgb(var(--color-border));
|
|
487
|
+
border-radius: 0.5rem;
|
|
488
|
+
background-color: rgb(var(--color-muted));
|
|
489
|
+
cursor: zoom-in;
|
|
490
|
+
object-fit: contain;
|
|
491
|
+
}
|
|
492
|
+
|
|
471
493
|
.rt-file {
|
|
472
494
|
display: inline-flex;
|
|
473
495
|
align-items: center;
|