@seafile/sea-email-editor 0.0.9 → 0.0.10-beta1

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/README.md CHANGED
@@ -1 +1,52 @@
1
1
  # Sea email editor
2
+
3
+ Sea email editor is a React-based rich text editor for composing email content. It is built on top of Slate and includes HTML and Markdown conversion, along with support for common email editing blocks.
4
+
5
+ ## Features
6
+
7
+ - HTML to editor content conversion
8
+ - Markdown to editor content conversion
9
+ - Rich text formatting: bold, italic, underline, inline code
10
+ - Block elements: headings, blockquote, lists, code blocks, divider, tables
11
+ - Link and image rendering
12
+ - Localization support
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @seafile/sea-email-editor
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```jsx
23
+ import React from "react";
24
+ import SeaEmailEditor from "@seafile/sea-email-editor";
25
+
26
+ export default function App() {
27
+ return (
28
+ <SeaEmailEditor
29
+ value={"<div>Hello, world!</div>"}
30
+ isHtmlValue={true}
31
+ onChange={(html) => {
32
+ console.log(html);
33
+ }}
34
+ />
35
+ );
36
+ }
37
+ ```
38
+
39
+ ## Props
40
+
41
+ - `value`: initial editor content, HTML or Markdown depending on `isHtmlValue`
42
+ - `isHtmlValue`: set to `true` when `value` is HTML
43
+ - `options.lang`: locale, currently `en` and `zh-cn`
44
+ - `onChange`: called with serialized HTML when the document changes
45
+ - `assetURLPrefix`: prefix for editor assets
46
+ - `editorApi`: editor integration hooks
47
+ - `onLinkClick`: custom link click handler
48
+
49
+ ## Notes
50
+
51
+ - The editor now re-initializes when the incoming `value` or `isHtmlValue` changes.
52
+ - The example app under `example/` demonstrates how to preview the generated HTML output.
@@ -12,6 +12,7 @@
12
12
  flex: 1;
13
13
  width: 100%;
14
14
  padding: 10px;
15
+ overflow-y: scroll;
15
16
  }
16
17
 
17
18
  .sea-email-editor .sea-email-editor-content *:first-child {
@@ -75,13 +75,19 @@ const Main = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
75
75
  const [firstNode] = editor.children;
76
76
  if (!firstNode) return;
77
77
  if (focusRange && focusRange !== null && focusRange !== void 0 && focusRange.anchor) {
78
- const startOfFirstNode = _slate.Editor.start(editor, focusRange.anchor.path);
79
- const range = {
80
- anchor: startOfFirstNode,
81
- focus: startOfFirstNode
82
- };
83
- (0, _core.focusEditor)(editor, range);
84
- setTimeout(() => (0, _core.focusEditor)(editor, focusRange), 0);
78
+ try {
79
+ const startOfFirstNode = _slate.Editor.start(editor, focusRange.anchor.path);
80
+ const range = {
81
+ anchor: startOfFirstNode,
82
+ focus: startOfFirstNode
83
+ };
84
+ (0, _core.focusEditor)(editor, range);
85
+ setTimeout(() => (0, _core.focusEditor)(editor, focusRange), 0);
86
+ } catch {
87
+ focusRangeRef.current = null;
88
+ focusNode(editor);
89
+ return;
90
+ }
85
91
  focusRangeRef.current = null;
86
92
  return;
87
93
  }
@@ -49,6 +49,21 @@ const renderText = (props, editor) => {
49
49
  children: markedChildren
50
50
  });
51
51
  }
52
+ if (leaf.superscript) {
53
+ markedChildren = /*#__PURE__*/(0, _jsxRuntime.jsx)("sup", {
54
+ children: markedChildren
55
+ });
56
+ }
57
+ if (leaf.subscript) {
58
+ markedChildren = /*#__PURE__*/(0, _jsxRuntime.jsx)("sub", {
59
+ children: markedChildren
60
+ });
61
+ }
62
+ if (leaf.highlight) {
63
+ markedChildren = /*#__PURE__*/(0, _jsxRuntime.jsx)("mark", {
64
+ children: markedChildren
65
+ });
66
+ }
52
67
  if (leaf.decoration) {
53
68
  markedChildren = /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
54
69
  children: markedChildren
@@ -10,6 +10,17 @@ var _typeOf = _interopRequireDefault(require("type-of"));
10
10
  var _constants = require("./constants");
11
11
  var _rules = _interopRequireDefault(require("./rules"));
12
12
  var _helper = require("./helper");
13
+ var _dom = require("../../utils/dom");
14
+ const generateDefaultValue = () => {
15
+ return [{
16
+ id: _slugid.default.nice(),
17
+ type: _constants.PARAGRAPH,
18
+ children: [{
19
+ text: '',
20
+ id: _slugid.default.nice()
21
+ }]
22
+ }];
23
+ };
13
24
  const cruftNewline = element => {
14
25
  return !(element.nodeName === '#text' && element.nodeValue === '\n');
15
26
  };
@@ -98,14 +109,7 @@ const deserializeElements = function () {
98
109
 
99
110
  const formatElementNodes = nodes => {
100
111
  if (nodes.length === 0) {
101
- return [{
102
- id: _slugid.default.nice(),
103
- type: _constants.PARAGRAPH,
104
- children: [{
105
- text: '',
106
- id: _slugid.default.nice()
107
- }]
108
- }];
112
+ return generateDefaultValue();
109
113
  }
110
114
  nodes = nodes.reduce((memo, node) => {
111
115
  if (_constants.TOP_LEVEL_TYPES.includes(node.type)) {
@@ -145,8 +149,8 @@ const formatElementNodes = nodes => {
145
149
  return nodes;
146
150
  };
147
151
  const deserializeHtml = html => {
148
- const parsed = new DOMParser().parseFromString(html.replace('\n\n', '').replace('\n ', ''), 'text/html');
149
- const fragment = parsed.body;
152
+ const fragment = (0, _dom.sanitizeHTMLContent)(html);
153
+ if (!fragment) return generateDefaultValue();
150
154
  const children = Array.from(fragment.childNodes);
151
155
  let nodes = [];
152
156
  nodes = deserializeElements(children, true);
@@ -17,7 +17,10 @@ const blockquoteRule = (element, parseChild) => {
17
17
  const node = {
18
18
  id: _slugid.default.nice(),
19
19
  type: _constants.BLOCKQUOTE,
20
- children: parseChild(childNodes)
20
+ children: parseChild(childNodes) || [{
21
+ id: _slugid.default.nice(),
22
+ text: ''
23
+ }]
21
24
  };
22
25
  return (0, _helper.mergeElementOther2SlateNode)(element, node);
23
26
  }
@@ -17,7 +17,10 @@ const codeBlockRule = (element, parseChild) => {
17
17
  const children = Array.from(childNodes).filter(item => item.nodeName === 'CODE');
18
18
  let codeChild = children[0];
19
19
  if (codeChild) {
20
- let lang = codeChild.getAttribute('lang');
20
+ let lang = codeChild.getAttribute('lang') || codeChild.getAttribute('class') || '';
21
+ if (lang.startsWith('language-')) {
22
+ lang = lang.replace('language-', '');
23
+ }
21
24
  lang = (0, _helper.genCodeLangs)().find(item => item.value === lang) || 'plaintext';
22
25
  const node = {
23
26
  id: _slugid.default.nice(),
@@ -27,9 +30,10 @@ const codeBlockRule = (element, parseChild) => {
27
30
  };
28
31
  return (0, _helper.mergeElementOther2SlateNode)(element, node);
29
32
  } else {
33
+ var _childNodes$;
30
34
  const lang = 'plaintext';
31
- const content = childNodes[0].textContent;
32
- const textArr = content.split('\n').filter(Boolean);
35
+ const content = ((_childNodes$ = childNodes[0]) === null || _childNodes$ === void 0 ? void 0 : _childNodes$.textContent) || '';
36
+ const textArr = content.split('\n');
33
37
  const children = textArr.map(text => {
34
38
  return {
35
39
  id: _slugid.default.nice(),
@@ -17,7 +17,11 @@ const imageRule = (element, parseChild) => {
17
17
  id: _slugid.default.nice(),
18
18
  type: _constants.IMAGE,
19
19
  data: {
20
- src: element.getAttribute('src')
20
+ src: element.getAttribute('src'),
21
+ alt: element.getAttribute('alt') || undefined,
22
+ title: element.getAttribute('title') || undefined,
23
+ width: element.getAttribute('width') || undefined,
24
+ height: element.getAttribute('height') || undefined
21
25
  },
22
26
  children: [{
23
27
  text: '',
@@ -14,12 +14,13 @@ const linkRule = (element, parseChild) => {
14
14
  } = element;
15
15
  const content = element.textContent || element.getAttribute('title') || element.getAttribute('href');
16
16
  if (nodeName === 'A') {
17
+ const children = parseChild(element.childNodes);
17
18
  const node = {
18
19
  id: _slugid.default.nice(),
19
20
  type: _constants.LINK,
20
21
  url: element.getAttribute('href') || content,
21
22
  title: element.getAttribute('title'),
22
- children: [{
23
+ children: Array.isArray(children) && children.length > 0 ? children : [{
23
24
  id: _slugid.default.nice(),
24
25
  text: content || ''
25
26
  }]
@@ -53,7 +53,7 @@ const listRule = (element, parseChild) => {
53
53
  };
54
54
  normalizedChildren.forEach(child => {
55
55
  if (!child) return;
56
- const isInlineNode = !child.type || _constants.INLINE_LEVEL_TYPES.includes(child.type);
56
+ const isInlineNode = !child.type || _constants.INLINE_LEVEL_TYPES.includes(child.type) || child.type === 'text';
57
57
  if (isInlineNode) {
58
58
  inlineChildren.push(child);
59
59
  return;
@@ -25,10 +25,17 @@ const pRule = (element, parseChild) => {
25
25
  };
26
26
  return (0, _helper.mergeElementOther2SlateNode)(element, node);
27
27
  }
28
+ let children = parseChild(childNodes);
29
+ if (children.length === 0) {
30
+ children = [{
31
+ id: _slugid.default.nice(),
32
+ text: ''
33
+ }];
34
+ }
28
35
  const node = {
29
36
  id: _slugid.default.nice(),
30
37
  type: _constants.P,
31
- children: parseChild(childNodes)
38
+ children
32
39
  };
33
40
  return (0, _helper.mergeElementOther2SlateNode)(element, node);
34
41
  }
@@ -13,7 +13,8 @@ const paragraphRule = (element, parseChild) => {
13
13
  nodeName,
14
14
  childNodes
15
15
  } = element;
16
- if (nodeName === 'DIV' && element.parentElement.nodeName !== 'LI') {
16
+ // article
17
+ if ((nodeName === 'DIV' || nodeName === 'ARTICLE') && element.parentElement.nodeName !== 'LI') {
17
18
  if (childNodes.length === 0) {
18
19
  const node = {
19
20
  id: _slugid.default.nice(),
@@ -25,10 +26,14 @@ const paragraphRule = (element, parseChild) => {
25
26
  };
26
27
  return (0, _helper.mergeElementOther2SlateNode)(element, node);
27
28
  }
29
+ const children = parseChild(childNodes);
28
30
  const node = {
29
31
  id: _slugid.default.nice(),
30
32
  type: _constants.PARAGRAPH,
31
- children: parseChild(childNodes)
33
+ children: children.length === 0 ? [{
34
+ id: _slugid.default.nice(),
35
+ text: ''
36
+ }] : children
32
37
  };
33
38
  return (0, _helper.mergeElementOther2SlateNode)(element, node);
34
39
  }
@@ -12,59 +12,65 @@ const textRule = (element, parseChild) => {
12
12
  nodeName,
13
13
  nodeType
14
14
  } = element;
15
- if (nodeName === 'SPAN') {
15
+ const createTextNode = function () {
16
+ let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
16
17
  const node = {
17
18
  id: _slugid.default.nice(),
18
- text: element.textContent
19
+ text: element.textContent,
20
+ ...props
19
21
  };
20
22
  return (0, _helper.mergeElementOther2SlateNode)(element, node);
23
+ };
24
+ if (nodeName === 'SPAN') {
25
+ return createTextNode();
21
26
  }
22
27
  if (nodeName === 'STRONG' || nodeName === 'B') {
23
- const node = {
24
- id: _slugid.default.nice(),
25
- bold: true,
26
- text: element.textContent
27
- };
28
- return (0, _helper.mergeElementOther2SlateNode)(element, node);
28
+ return createTextNode({
29
+ bold: true
30
+ });
29
31
  }
30
32
  if (nodeName === 'CODE' && element.parentElement.nodeName !== 'PRE') {
31
- const node = {
32
- id: _slugid.default.nice(),
33
- code: true,
34
- text: element.textContent
35
- };
36
- return (0, _helper.mergeElementOther2SlateNode)(element, node);
33
+ return createTextNode({
34
+ code: true
35
+ });
37
36
  }
38
- if (nodeName === 'DEL') {
39
- const node = {
40
- id: _slugid.default.nice(),
41
- delete: true,
42
- text: element.textContent
43
- };
44
- return (0, _helper.mergeElementOther2SlateNode)(element, node);
37
+ if (nodeName === 'DEL' || nodeName === 'S' || nodeName === 'STRIKE') {
38
+ return createTextNode({
39
+ delete: true
40
+ });
45
41
  }
46
- if (nodeName === 'I') {
47
- const node = {
48
- id: _slugid.default.nice(),
49
- italic: true,
50
- text: element.textContent
51
- };
52
- return (0, _helper.mergeElementOther2SlateNode)(element, node);
42
+ if (nodeName === 'I' || nodeName === 'EM') {
43
+ return createTextNode({
44
+ italic: true
45
+ });
53
46
  }
54
47
  if (nodeName === 'INS') {
55
- const node = {
56
- id: _slugid.default.nice(),
57
- add: true,
58
- text: element.textContent
59
- };
60
- return (0, _helper.mergeElementOther2SlateNode)(element, node);
48
+ return createTextNode({
49
+ add: true
50
+ });
51
+ }
52
+ if (nodeName === 'U') {
53
+ return createTextNode({
54
+ underline: true
55
+ });
56
+ }
57
+ if (nodeName === 'SUP') {
58
+ return createTextNode({
59
+ superscript: true
60
+ });
61
+ }
62
+ if (nodeName === 'SUB') {
63
+ return createTextNode({
64
+ subscript: true
65
+ });
66
+ }
67
+ if (nodeName === 'MARK') {
68
+ return createTextNode({
69
+ highlight: true
70
+ });
61
71
  }
62
72
  if (nodeType === 3) {
63
- const node = {
64
- id: _slugid.default.nice(),
65
- text: element.textContent
66
- };
67
- return (0, _helper.mergeElementOther2SlateNode)(element, node);
73
+ return createTextNode();
68
74
  }
69
75
  return;
70
76
  };
@@ -11,7 +11,8 @@ var _elementTypes = require("../../extension/constants/element-types");
11
11
  var _htmlToSlate = _interopRequireDefault(require("../html-to-slate"));
12
12
  const INLINE_KEY_MAP = {
13
13
  strong: 'bold',
14
- emphasis: 'italic'
14
+ emphasis: 'italic',
15
+ delete: 'delete'
15
16
  };
16
17
 
17
18
  // <strong><em>aa<em>bb<em></strong>
@@ -88,6 +89,15 @@ const applyMarkForInlineItem = function (result, item) {
88
89
  textNode = {};
89
90
  return;
90
91
  }
92
+ if (type === 'break') {
93
+ result.push({
94
+ id: _slugid.default.nice(),
95
+ text: '\n',
96
+ ...textNode
97
+ });
98
+ textNode = {};
99
+ return;
100
+ }
91
101
  if (type === 'inlineCode') {
92
102
  textNode['code'] = true;
93
103
  textNode['text'] = value || '';
@@ -430,16 +440,16 @@ const transformMath = node => {
430
440
  };
431
441
  exports.transformMath = transformMath;
432
442
  const elementHandlers = {
433
- 'paragraph': transformParagraph,
434
- 'heading': transformHeader,
435
- 'blockquote': transformBlockquote,
436
- 'table': transformTable,
437
- 'list': transformList,
443
+ paragraph: transformParagraph,
444
+ heading: transformHeader,
445
+ blockquote: transformBlockquote,
446
+ table: transformTable,
447
+ list: transformList,
438
448
  // ordered_list | unordered_list | check_list_item
439
- 'code': transformCodeBlock,
440
- 'thematicBreak': transformHr,
441
- 'math': transformMath,
442
- 'html': transformBlockHtml
449
+ code: transformCodeBlock,
450
+ thematicBreak: transformHr,
451
+ math: transformMath,
452
+ html: transformBlockHtml
443
453
  };
444
454
  const formatMdToSlate = children => {
445
455
  const validChildren = children.filter(child => elementHandlers[child.type]);
@@ -12,8 +12,7 @@ const isContentValid = value => {
12
12
  };
13
13
  const isEmptyParagraph = node => {
14
14
  const voidNodeTypes = ['image', 'formula'];
15
- if (node.type !== _constants.ElementTypes.PARAGRAPH) return false;
16
- if (node.type !== _constants.ElementTypes.P) return false;
15
+ if (![_constants.ElementTypes.PARAGRAPH, _constants.ElementTypes.P].includes(node.type)) return false;
17
16
  const hasBlock = node.children.some(item => voidNodeTypes.includes(item.type));
18
17
  const hasHtml = node.children.some(item => item.type === 'html');
19
18
  if (hasBlock) return false;
@@ -128,6 +127,21 @@ const element2Html = (value, element, path) => {
128
127
  if (underline) {
129
128
  textDom = `<span style="text-decoration: underline;">${textDom}</span>`;
130
129
  }
130
+ if (element.delete) {
131
+ textDom = `<del>${textDom}</del>`;
132
+ }
133
+ if (element.add) {
134
+ textDom = `<ins>${textDom}</ins>`;
135
+ }
136
+ if (element.superscript) {
137
+ textDom = `<sup>${textDom}</sup>`;
138
+ }
139
+ if (element.subscript) {
140
+ textDom = `<sub>${textDom}</sub>`;
141
+ }
142
+ if (element.highlight) {
143
+ textDom = `<mark>${textDom}</mark>`;
144
+ }
131
145
  if (code) {
132
146
  const {
133
147
  style
@@ -33,8 +33,10 @@ const isUrl = url => {
33
33
  // Check document is empty or only contains void nodes
34
34
  exports.isUrl = isUrl;
35
35
  const isDocumentEmpty = editor => {
36
- const document = editor.children;
36
+ const document = editor === null || editor === void 0 ? void 0 : editor.children;
37
+ if (!Array.isArray(document) || document.length === 0) return true;
37
38
  const [firstChildNode] = document;
39
+ if (!firstChildNode || !Array.isArray(firstChildNode.children)) return true;
38
40
  // Check if document has only one block node
39
41
  const isWrapperEmpty = document.length === 1 && _slate.Node.string(firstChildNode).length === 0;
40
42
  if (!isWrapperEmpty) return false;
package/dist/utils/dom.js CHANGED
@@ -8,7 +8,8 @@ exports.getTarget = exports.getEventClassName = exports.getDataAttr = exports.ca
8
8
  exports.hasClass = hasClass;
9
9
  exports.isNearBottom = exports.isInputOrEditorActive = void 0;
10
10
  exports.removeClass = removeClass;
11
- exports.removeClassName = void 0;
11
+ exports.sanitizeHTMLContent = exports.removeClassName = void 0;
12
+ var _typeDetection = require("./type-detection");
12
13
  const canUseDOM = exports.canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
13
14
  const getEventClassName = e => {
14
15
  // svg mouseEvent event.target.className is an object
@@ -130,4 +131,28 @@ const isNearBottom = function (element) {
130
131
  const distanceToBottom = scrollHeight - (scrollTop + clientHeight);
131
132
  return distanceToBottom <= threshold;
132
133
  };
133
- exports.isNearBottom = isNearBottom;
134
+ exports.isNearBottom = isNearBottom;
135
+ const removeCommentNodes = node => {
136
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
137
+ const child = node.childNodes[i];
138
+ if (child.nodeType === Node.COMMENT_NODE) {
139
+ node.removeChild(child);
140
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
141
+ removeCommentNodes(child);
142
+ }
143
+ }
144
+ };
145
+ const sanitizeHTMLContent = function () {
146
+ let html = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
147
+ if ((0, _typeDetection.isNumber)(html)) {
148
+ html = String(html);
149
+ }
150
+ if (!(0, _typeDetection.isString)(html)) return null;
151
+ const sanitizedHTML = html.replace(/<!--([\s\S]*?)-->/g, '').replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '').replace(/<title\b[^>]*>[\s\S]*?<\/title>/gi, '').replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '').replace(/\s*[\n\t]\s*/g, '');
152
+ const parsed = new DOMParser().parseFromString(sanitizedHTML, 'text/html');
153
+ const body = parsed.body;
154
+ body.querySelectorAll('style, title, script').forEach(el => el.remove());
155
+ removeCommentNodes(body);
156
+ return body;
157
+ };
158
+ exports.sanitizeHTMLContent = sanitizeHTMLContent;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@seafile/sea-email-editor",
3
- "version": "0.0.9",
4
- "description": "",
3
+ "version": "0.0.10-beta1",
4
+ "description": "A Slate-based rich email editor with HTML, markdown, tables, images, links, and block plugins.",
5
5
  "main": "dist/index.js",
6
6
  "dependencies": {
7
- "@seafile/react-image-lightbox": "^5.0.4",
7
+ "@seafile/react-image-lightbox": "^5.0.4",
8
8
  "classnames": "2.3.2",
9
9
  "copy-to-clipboard": "3.3.1",
10
10
  "deep-copy": "1.4.2",