@tarviks/lexical-rich-editor 1.3.10 → 1.3.12

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/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
6
6
  import { useLexicalEditable } from '@lexical/react/useLexicalEditable';
7
7
  import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
8
- import { createCommand, DecoratorNode, COMMAND_PRIORITY_HIGH, TextNode, ElementNode, SELECTION_CHANGE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, $getSelection, $isRangeSelection, $findMatchingParent, COMMAND_PRIORITY_LOW, KEY_DOWN_COMMAND, $getNodeByKey, $createRangeSelection, $setSelection, $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_EDITOR, PASTE_COMMAND, DRAGSTART_COMMAND, DRAGOVER_COMMAND, DROP_COMMAND, $getRoot, $createTextNode, $isTextNode, $isElementNode, FORMAT_ELEMENT_COMMAND, CLICK_COMMAND, $isNodeSelection, $applyNodeReplacement, FORMAT_TEXT_COMMAND, $isDecoratorNode, $getNearestNodeFromDOMNode, COMMAND_PRIORITY_CRITICAL, REDO_COMMAND, UNDO_COMMAND, getDOMSelection, KEY_ESCAPE_COMMAND, isHTMLElement, getDOMSelectionFromTarget, $isLineBreakNode, SKIP_SELECTION_FOCUS_TAG, createEditor, KEY_ENTER_COMMAND } from 'lexical';
8
+ import { createCommand, $isTextNode, DecoratorNode, COMMAND_PRIORITY_HIGH, TextNode, ElementNode, SELECTION_CHANGE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, $getSelection, $isRangeSelection, $findMatchingParent, COMMAND_PRIORITY_LOW, KEY_DOWN_COMMAND, $getNodeByKey, $createRangeSelection, $setSelection, $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_EDITOR, PASTE_COMMAND, DRAGSTART_COMMAND, DRAGOVER_COMMAND, DROP_COMMAND, $getRoot, $createTextNode, $isElementNode, FORMAT_ELEMENT_COMMAND, CLICK_COMMAND, $isNodeSelection, $applyNodeReplacement, FORMAT_TEXT_COMMAND, $isDecoratorNode, $getNearestNodeFromDOMNode, COMMAND_PRIORITY_CRITICAL, REDO_COMMAND, UNDO_COMMAND, getDOMSelection, KEY_ESCAPE_COMMAND, isHTMLElement, getDOMSelectionFromTarget, $isLineBreakNode, SKIP_SELECTION_FOCUS_TAG, createEditor, KEY_ENTER_COMMAND } from 'lexical';
9
9
  import { mergeStyleSets, Stack, css, useTheme, Callout, TextField } from '@fluentui/react';
10
10
  import { makeStyles, FluentProvider, webLightTheme, Menu, MenuTrigger, MenuPopover, MenuList, MenuGroup, MenuGroupHeader, MenuItem, MenuDivider, Dropdown, Option, Button, ToolbarDivider, MenuItemRadio, Field, Input, MessageBar, MessageBarBody, Popover, PopoverSurface, PopoverTrigger } from '@fluentui/react-components';
11
11
  import { CodeHighlightNode, CodeNode, $isCodeHighlightNode } from '@lexical/code';
@@ -885,6 +885,7 @@ var init_InlineImageComponent = __esm({
885
885
  "data-position": position,
886
886
  style: {
887
887
  display: "block",
888
+ maxWidth: "100%",
888
889
  height,
889
890
  width
890
891
  },
@@ -1074,14 +1075,17 @@ var init_InlineImageComponent = __esm({
1074
1075
  InlineImageComponent_default = InlineImageComponent;
1075
1076
  }
1076
1077
  });
1077
- var InlineImageComponent2, $convertInlineImageElement, InlineImageNode, $createInlineImageNode, $isInlineImageNode;
1078
+ var InlineImageComponent2, INLINE_IMAGE_MARKER_ATTR, $convertInlineImageElement, InlineImageNode, $createInlineImageNode, $isInlineImageNode;
1078
1079
  var init_InlineImageNode = __esm({
1079
1080
  "src/Nodes/InlineImageNode.tsx"() {
1080
1081
  InlineImageComponent2 = React9.lazy(() => Promise.resolve().then(() => (init_InlineImageComponent(), InlineImageComponent_exports)));
1082
+ INLINE_IMAGE_MARKER_ATTR = "data-lexical-inline-image";
1081
1083
  $convertInlineImageElement = (domNode) => {
1082
1084
  if (isHTMLElement(domNode) && domNode.nodeName === "IMG") {
1083
1085
  const { alt: altText, src, width, height } = domNode;
1084
- const node = $createInlineImageNode({ altText, height, src, width });
1086
+ const positionAttr = domNode.getAttribute("data-position");
1087
+ const position = positionAttr === "left" || positionAttr === "right" || positionAttr === "full" ? positionAttr : void 0;
1088
+ const node = $createInlineImageNode({ altText, height, position, src, width });
1085
1089
  return { node };
1086
1090
  }
1087
1091
  return null;
@@ -1125,10 +1129,15 @@ var init_InlineImageNode = __esm({
1125
1129
  }
1126
1130
  static importDOM() {
1127
1131
  return {
1128
- img: (node) => ({
1129
- conversion: $convertInlineImageElement,
1130
- priority: 0
1131
- })
1132
+ img: (node) => {
1133
+ if (isHTMLElement(node) && node.hasAttribute(INLINE_IMAGE_MARKER_ATTR)) {
1134
+ return {
1135
+ conversion: $convertInlineImageElement,
1136
+ priority: 3
1137
+ };
1138
+ }
1139
+ return null;
1140
+ }
1132
1141
  };
1133
1142
  }
1134
1143
  constructor(src, altText, position, width, height, showCaption, caption, key) {
@@ -1147,6 +1156,11 @@ var init_InlineImageNode = __esm({
1147
1156
  element.setAttribute("alt", this.__altText);
1148
1157
  element.setAttribute("width", this.__width.toString());
1149
1158
  element.setAttribute("height", this.__height.toString());
1159
+ element.setAttribute(INLINE_IMAGE_MARKER_ATTR, "true");
1160
+ if (this.__position) {
1161
+ element.setAttribute("data-position", this.__position);
1162
+ _InlineImageNode.applyPositionStyle(element, this.__position);
1163
+ }
1150
1164
  return { element };
1151
1165
  }
1152
1166
  exportJSON() {
@@ -1204,12 +1218,34 @@ var init_InlineImageNode = __esm({
1204
1218
  }
1205
1219
  }
1206
1220
  // View
1221
+ // The position-left/right/full behavior is also defined in the package's
1222
+ // CSS file as `.inline-editor-image.position-*` rules, but that file is a
1223
+ // separate import (`@tarviks/lexical-rich-editor/dist/index.css`) a
1224
+ // consuming app can forget to include. Applying it as inline styles here
1225
+ // too means positioning still works even when that stylesheet isn't
1226
+ // loaded, instead of silently no-op'ing back to normal block flow.
1227
+ static applyPositionStyle(span, position) {
1228
+ span.style.removeProperty("float");
1229
+ span.style.removeProperty("display");
1230
+ span.style.removeProperty("margin");
1231
+ if (position === "left") {
1232
+ span.style.float = "left";
1233
+ span.style.margin = "4px 8px 4px 0";
1234
+ } else if (position === "right") {
1235
+ span.style.float = "right";
1236
+ span.style.margin = "4px 0 4px 8px";
1237
+ } else if (position === "full") {
1238
+ span.style.display = "block";
1239
+ span.style.margin = "8px 0";
1240
+ }
1241
+ }
1207
1242
  createDOM(config) {
1208
1243
  const span = document.createElement("span");
1209
1244
  const className = `${config.theme.inlineImage} position-${this.__position}`;
1210
1245
  if (className !== void 0) {
1211
1246
  span.className = className;
1212
1247
  }
1248
+ _InlineImageNode.applyPositionStyle(span, this.__position);
1213
1249
  return span;
1214
1250
  }
1215
1251
  updateDOM(prevNode, dom, config) {
@@ -1219,6 +1255,7 @@ var init_InlineImageNode = __esm({
1219
1255
  if (className !== void 0) {
1220
1256
  dom.className = className;
1221
1257
  }
1258
+ _InlineImageNode.applyPositionStyle(dom, position);
1222
1259
  }
1223
1260
  return false;
1224
1261
  }
@@ -2172,6 +2209,48 @@ function $createYouTubeNode(videoID, width = DEFAULT_WIDTH, height = DEFAULT_HEI
2172
2209
  function $isYouTubeNode(node) {
2173
2210
  return node instanceof YouTubeNode;
2174
2211
  }
2212
+ var PRESERVED_STYLE_PROPS = [
2213
+ ["fontFamily", "font-family"],
2214
+ ["fontSize", "font-size"],
2215
+ ["color", "color"],
2216
+ ["backgroundColor", "background-color"]
2217
+ ];
2218
+ var $convertSpanElementWithStyle = (domNode) => {
2219
+ const style = domNode.style;
2220
+ const cssParts = [];
2221
+ for (const [domProp, cssProp] of PRESERVED_STYLE_PROPS) {
2222
+ const value = style[domProp];
2223
+ if (value) cssParts.push(`${cssProp}: ${value}`);
2224
+ }
2225
+ const cssText = cssParts.length > 0 ? `${cssParts.join("; ")};` : "";
2226
+ const fontWeight = style.fontWeight;
2227
+ const textDecoration = style.textDecoration.split(" ");
2228
+ const hasBoldFontWeight = fontWeight === "700" || fontWeight === "bold";
2229
+ const hasLinethroughTextDecoration = textDecoration.includes("line-through");
2230
+ const hasItalicFontStyle = style.fontStyle === "italic";
2231
+ const hasUnderlineTextDecoration = textDecoration.includes("underline");
2232
+ const verticalAlign = style.verticalAlign;
2233
+ return {
2234
+ node: null,
2235
+ forChild: (lexicalNode) => {
2236
+ if (!$isTextNode(lexicalNode)) return lexicalNode;
2237
+ if (cssText) lexicalNode.setStyle(cssText);
2238
+ if (hasBoldFontWeight && !lexicalNode.hasFormat("bold")) lexicalNode.toggleFormat("bold");
2239
+ if (hasLinethroughTextDecoration && !lexicalNode.hasFormat("strikethrough")) lexicalNode.toggleFormat("strikethrough");
2240
+ if (hasItalicFontStyle && !lexicalNode.hasFormat("italic")) lexicalNode.toggleFormat("italic");
2241
+ if (hasUnderlineTextDecoration && !lexicalNode.hasFormat("underline")) lexicalNode.toggleFormat("underline");
2242
+ if (verticalAlign === "sub" && !lexicalNode.hasFormat("subscript")) lexicalNode.toggleFormat("subscript");
2243
+ if (verticalAlign === "super" && !lexicalNode.hasFormat("superscript")) lexicalNode.toggleFormat("superscript");
2244
+ return lexicalNode;
2245
+ }
2246
+ };
2247
+ };
2248
+ var preserveTextStyleImportMap = {
2249
+ span: () => ({
2250
+ conversion: $convertSpanElementWithStyle,
2251
+ priority: 0
2252
+ })
2253
+ };
2175
2254
  var normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim();
2176
2255
  function readContextWindow(maxChars = 300, cursorIndex) {
2177
2256
  const full = normalize($getRoot().getTextContent());
@@ -2945,6 +3024,8 @@ var ALLOWED_ATTRS = /* @__PURE__ */ new Set([
2945
3024
  "data-lex-block",
2946
3025
  "data-kind",
2947
3026
  "data-lexical-decorator",
3027
+ "data-lexical-inline-image",
3028
+ "data-position",
2948
3029
  // Misc
2949
3030
  "title",
2950
3031
  "allowfullscreen"
@@ -5099,11 +5180,37 @@ function SpellCheckPlugin({
5099
5180
  }
5100
5181
  ) : null;
5101
5182
  }
5183
+ function getScrollClipAncestor(el) {
5184
+ let node = el.parentElement;
5185
+ while (node) {
5186
+ const style = getComputedStyle(node);
5187
+ if ((style.overflowY === "auto" || style.overflowY === "scroll") && node.scrollHeight > node.clientHeight) {
5188
+ return node;
5189
+ }
5190
+ node = node.parentElement;
5191
+ }
5192
+ return null;
5193
+ }
5194
+ var BUTTON_VERTICAL_FOOTPRINT = 34;
5195
+ function isRectVisible(rect, cellDom) {
5196
+ if (rect.width === 0 && rect.height === 0) return false;
5197
+ const clipAncestor = getScrollClipAncestor(cellDom);
5198
+ const containerRect = clipAncestor?.getBoundingClientRect();
5199
+ const visibleTop = containerRect ? Math.max(0, containerRect.top) : 0;
5200
+ const visibleBottom = containerRect ? Math.min(window.innerHeight, containerRect.bottom) : window.innerHeight;
5201
+ const visibleLeft = containerRect ? Math.max(0, containerRect.left) : 0;
5202
+ const visibleRight = containerRect ? Math.min(window.innerWidth, containerRect.right) : window.innerWidth;
5203
+ if (rect.top < visibleTop) return false;
5204
+ if (rect.top + BUTTON_VERTICAL_FOOTPRINT > visibleBottom) return false;
5205
+ if (rect.left >= visibleRight || rect.right <= visibleLeft) return false;
5206
+ return true;
5207
+ }
5102
5208
  function TableActionMenuPlugin({ disabled = false }) {
5103
5209
  const [editor] = useLexicalComposerContext();
5104
5210
  const portalContainer = useFloatingPortalContainer(editor);
5105
5211
  const [isInTable, setIsInTable] = React9.useState(false);
5106
5212
  const [anchorRect, setAnchorRect] = React9.useState(null);
5213
+ const [isAnchorVisible, setIsAnchorVisible] = React9.useState(false);
5107
5214
  const [contentRight, setContentRight] = React9.useState(null);
5108
5215
  const [open, setOpen] = React9.useState(false);
5109
5216
  const openRef = React9.useRef(false);
@@ -5129,8 +5236,10 @@ function TableActionMenuPlugin({ disabled = false }) {
5129
5236
  if (tableNode) {
5130
5237
  const dom = editor.getElementByKey(tableNode.getKey());
5131
5238
  if (dom) {
5239
+ const rect = dom.getBoundingClientRect();
5132
5240
  setIsInTable(true);
5133
- setAnchorRect(dom.getBoundingClientRect());
5241
+ setAnchorRect(rect);
5242
+ setIsAnchorVisible(isRectVisible(rect, dom));
5134
5243
  setContentRight(null);
5135
5244
  return;
5136
5245
  }
@@ -5139,6 +5248,7 @@ function TableActionMenuPlugin({ disabled = false }) {
5139
5248
  if (!$isRangeSelection(selection)) {
5140
5249
  setIsInTable(false);
5141
5250
  setAnchorRect(null);
5251
+ setIsAnchorVisible(false);
5142
5252
  setContentRight(null);
5143
5253
  return;
5144
5254
  }
@@ -5147,6 +5257,7 @@ function TableActionMenuPlugin({ disabled = false }) {
5147
5257
  if (!cellNode || !$isTableCellNode(cellNode)) {
5148
5258
  setIsInTable(false);
5149
5259
  setAnchorRect(null);
5260
+ setIsAnchorVisible(false);
5150
5261
  setContentRight(null);
5151
5262
  return;
5152
5263
  }
@@ -5154,11 +5265,14 @@ function TableActionMenuPlugin({ disabled = false }) {
5154
5265
  if (!cellDom) {
5155
5266
  setIsInTable(false);
5156
5267
  setAnchorRect(null);
5268
+ setIsAnchorVisible(false);
5157
5269
  setContentRight(null);
5158
5270
  return;
5159
5271
  }
5272
+ const cellRect = cellDom.getBoundingClientRect();
5160
5273
  setIsInTable(true);
5161
- setAnchorRect(cellDom.getBoundingClientRect());
5274
+ setAnchorRect(cellRect);
5275
+ setIsAnchorVisible(isRectVisible(cellRect, cellDom));
5162
5276
  setContentRight(measureContentRight(cellDom));
5163
5277
  if ($isRangeSelection(selection)) {
5164
5278
  savedAnchorRef.current = {
@@ -5237,7 +5351,7 @@ function TableActionMenuPlugin({ disabled = false }) {
5237
5351
  COMMAND_PRIORITY_HIGH
5238
5352
  );
5239
5353
  }, [editor, disabled]);
5240
- const canShow = isInTable && !!anchorRect && !disabled;
5354
+ const canShow = isInTable && !!anchorRect && isAnchorVisible && !disabled;
5241
5355
  const handleStyle = React9.useMemo(() => {
5242
5356
  if (!anchorRect) return void 0;
5243
5357
  const top = Math.max(8, anchorRect.top + 6);
@@ -8196,7 +8310,10 @@ var ContentEditorComponent = forwardRef(
8196
8310
  AutocompleteNode,
8197
8311
  SpellErrorNode,
8198
8312
  HtmlBlockNode
8199
- ]
8313
+ ],
8314
+ html: {
8315
+ import: preserveTextStyleImportMap
8316
+ }
8200
8317
  };
8201
8318
  props.onBeforeInitialize?.(config);
8202
8319
  return config;