@yurikilian/lex4 1.6.0 → 1.7.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.
@@ -2,11 +2,11 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
- import React, { createContext, useContext, useMemo, useReducer, useState, useRef, useCallback, useEffect, forwardRef, createElement, useImperativeHandle } from "react";
6
- import { $getRoot, $createRangeSelectionFromDom, $getSelection, $isRangeSelection, $isTextNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $createParagraphNode, $applyNodeReplacement, DecoratorNode, KEY_BACKSPACE_COMMAND, COMMAND_PRIORITY_LOW, KEY_DELETE_COMMAND, KEY_DOWN_COMMAND, $isNodeSelection, $getNodeByKey, $selectAll, SELECTION_CHANGE_COMMAND, KEY_TAB_COMMAND, $isElementNode, $isParagraphNode, $setSelection, FOCUS_COMMAND, $splitNode, $getNearestNodeFromDOMNode, CONTROLLED_TEXT_INSERTION_COMMAND, PASTE_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_CRITICAL, $insertNodes, $createLineBreakNode, $createTextNode, createCommand, COMMAND_PRIORITY_EDITOR } from "lexical";
5
+ import React, { createContext, useContext, useMemo, useRef, useReducer, useState, useCallback, useEffect, forwardRef, createElement, useImperativeHandle } from "react";
6
+ import { $getSelection, $isRangeSelection, $isTextNode, $getRoot, $createRangeSelectionFromDom, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $applyNodeReplacement, DecoratorNode, KEY_BACKSPACE_COMMAND, COMMAND_PRIORITY_LOW, KEY_DELETE_COMMAND, KEY_DOWN_COMMAND, $isNodeSelection, $getNodeByKey, $setSelection, $createNodeSelection, $isElementNode, $createParagraphNode, $selectAll, SELECTION_CHANGE_COMMAND, KEY_TAB_COMMAND, $isParagraphNode, FOCUS_COMMAND, $splitNode, $getNearestNodeFromDOMNode, CONTROLLED_TEXT_INSERTION_COMMAND, PASTE_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_CRITICAL, $insertNodes, $createLineBreakNode, $createTextNode, createCommand, COMMAND_PRIORITY_EDITOR } from "lexical";
7
7
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
8
8
  import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode, ListItemNode, $isListNode, $createListItemNode, $createListNode } from "@lexical/list";
9
- import { $setBlocksType } from "@lexical/selection";
9
+ import { $patchStyleText, $setBlocksType } from "@lexical/selection";
10
10
  import { $createHeadingNode, $isHeadingNode, HeadingNode, QuoteNode, $createQuoteNode } from "@lexical/rich-text";
11
11
  import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection";
12
12
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
@@ -777,6 +777,115 @@ const TranslationsProvider = ({
777
777
  );
778
778
  return /* @__PURE__ */ jsx(TranslationsContext.Provider, { value: merged, children });
779
779
  };
780
+ const createStoreImpl = (createState) => {
781
+ let state;
782
+ const listeners = /* @__PURE__ */ new Set();
783
+ const setState = (partial, replace) => {
784
+ const nextState = typeof partial === "function" ? partial(state) : partial;
785
+ if (!Object.is(nextState, state)) {
786
+ const previousState = state;
787
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
788
+ listeners.forEach((listener) => listener(state, previousState));
789
+ }
790
+ };
791
+ const getState = () => state;
792
+ const getInitialState = () => initialState;
793
+ const subscribe = (listener) => {
794
+ listeners.add(listener);
795
+ return () => listeners.delete(listener);
796
+ };
797
+ const api = { setState, getState, getInitialState, subscribe };
798
+ const initialState = state = createState(setState, getState, api);
799
+ return api;
800
+ };
801
+ const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
802
+ const identity = (arg) => arg;
803
+ function useStore(api, selector = identity) {
804
+ const slice = React.useSyncExternalStore(
805
+ api.subscribe,
806
+ React.useCallback(() => selector(api.getState()), [api, selector]),
807
+ React.useCallback(() => selector(api.getInitialState()), [api, selector])
808
+ );
809
+ React.useDebugValue(slice);
810
+ return slice;
811
+ }
812
+ const SUPPORTED_FONT_SIZES = [
813
+ 8,
814
+ 9,
815
+ 10,
816
+ 11,
817
+ 12,
818
+ 14,
819
+ 16,
820
+ 18,
821
+ 20,
822
+ 24,
823
+ 28,
824
+ 32,
825
+ 36,
826
+ 48,
827
+ 72
828
+ ];
829
+ const DEFAULT_FONT_SIZE = 12;
830
+ function applyFontSize(editor, size) {
831
+ editor.update(() => {
832
+ const selection = $getSelection();
833
+ if (!$isRangeSelection(selection)) return;
834
+ const nodes = selection.getNodes();
835
+ for (const node of nodes) {
836
+ if ($isTextNode(node)) {
837
+ const existing = node.getStyle();
838
+ const updated = mergeFontSize(existing, size);
839
+ node.setStyle(updated);
840
+ }
841
+ }
842
+ });
843
+ }
844
+ function mergeFontSize(existingStyle, size) {
845
+ const stripped = existingStyle.replace(/font-size:\s*[^;]+;?\s*/g, "").trim();
846
+ const sizeDecl = `font-size: ${size}pt`;
847
+ return stripped ? `${stripped}; ${sizeDecl}` : sizeDecl;
848
+ }
849
+ const DEFAULT_TOOLBAR_STYLE_SNAPSHOT = {
850
+ blockType: "paragraph",
851
+ fontFamily: "Calibri",
852
+ fontSize: DEFAULT_FONT_SIZE,
853
+ alignment: "left",
854
+ isBold: false,
855
+ isItalic: false,
856
+ isUnderline: false,
857
+ isStrikethrough: false,
858
+ hasSelectedVariable: false
859
+ };
860
+ function createToolbarStyleStore(initialSnapshot = DEFAULT_TOOLBAR_STYLE_SNAPSHOT) {
861
+ return createStore((set) => ({
862
+ ...initialSnapshot,
863
+ setSnapshot: (snapshot) => set(snapshot),
864
+ reset: () => set(DEFAULT_TOOLBAR_STYLE_SNAPSHOT)
865
+ }));
866
+ }
867
+ const ToolbarStyleStoreContext = createContext(null);
868
+ const ToolbarStyleStoreProvider = ({ children }) => {
869
+ const storeRef = useRef(null);
870
+ if (!storeRef.current) {
871
+ storeRef.current = createToolbarStyleStore();
872
+ }
873
+ return /* @__PURE__ */ jsx(ToolbarStyleStoreContext.Provider, { value: storeRef.current, children });
874
+ };
875
+ function useToolbarStyleStore(selector) {
876
+ const store = useContext(ToolbarStyleStoreContext);
877
+ if (!store) {
878
+ throw new Error("useToolbarStyleStore must be used within a ToolbarStyleStoreProvider");
879
+ }
880
+ return useStore(store, selector);
881
+ }
882
+ function useToolbarStyleStoreApi() {
883
+ const store = useContext(ToolbarStyleStoreContext);
884
+ if (!store) {
885
+ throw new Error("useToolbarStyleStoreApi must be used within a ToolbarStyleStoreProvider");
886
+ }
887
+ return store;
888
+ }
780
889
  const HISTORY_RESTORE_SUPPRESSION_MS = 100;
781
890
  const HISTORY_BATCH_FLUSH_MS = 16;
782
891
  function cloneDocumentSnapshot(document2) {
@@ -1333,7 +1442,7 @@ const DocumentProvider = ({
1333
1442
  undo,
1334
1443
  redo,
1335
1444
  editorRegistry
1336
- }, children });
1445
+ }, children: /* @__PURE__ */ jsx(ToolbarStyleStoreProvider, { children }) });
1337
1446
  };
1338
1447
  /**
1339
1448
  * @license lucide-react v1.8.0 - ISC
@@ -2081,43 +2190,6 @@ function applyFontFamily(editor, fontFamily) {
2081
2190
  }
2082
2191
  });
2083
2192
  }
2084
- const SUPPORTED_FONT_SIZES = [
2085
- 8,
2086
- 9,
2087
- 10,
2088
- 11,
2089
- 12,
2090
- 14,
2091
- 16,
2092
- 18,
2093
- 20,
2094
- 24,
2095
- 28,
2096
- 32,
2097
- 36,
2098
- 48,
2099
- 72
2100
- ];
2101
- const DEFAULT_FONT_SIZE = 12;
2102
- function applyFontSize(editor, size) {
2103
- editor.update(() => {
2104
- const selection = $getSelection();
2105
- if (!$isRangeSelection(selection)) return;
2106
- const nodes = selection.getNodes();
2107
- for (const node of nodes) {
2108
- if ($isTextNode(node)) {
2109
- const existing = node.getStyle();
2110
- const updated = mergeFontSize(existing, size);
2111
- node.setStyle(updated);
2112
- }
2113
- }
2114
- });
2115
- }
2116
- function mergeFontSize(existingStyle, size) {
2117
- const stripped = existingStyle.replace(/font-size:\s*[^;]+;?\s*/g, "").trim();
2118
- const sizeDecl = `font-size: ${size}pt`;
2119
- return stripped ? `${stripped}; ${sizeDecl}` : sizeDecl;
2120
- }
2121
2193
  function toggleFormat(editor, format) {
2122
2194
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
2123
2195
  }
@@ -2149,48 +2221,77 @@ function indentContent(editor) {
2149
2221
  function outdentContent(editor) {
2150
2222
  editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, void 0);
2151
2223
  }
2152
- function setBlockType(editor, blockType) {
2153
- editor.update(() => {
2154
- const selection = $getSelection();
2155
- if (!$isRangeSelection(selection)) {
2156
- return;
2157
- }
2158
- if (blockType === "paragraph") {
2159
- $setBlocksType(selection, () => $createParagraphNode());
2160
- return;
2161
- }
2162
- $setBlocksType(selection, () => $createHeadingNode(blockType));
2163
- });
2224
+ const INLINE_BLOCK_STYLE_PROPERTY = "--lex4-block-type";
2225
+ const INLINE_BLOCK_STYLE_PRESETS = {
2226
+ paragraph: {
2227
+ [INLINE_BLOCK_STYLE_PROPERTY]: "paragraph",
2228
+ "font-size": "12pt",
2229
+ "font-weight": "400"
2230
+ },
2231
+ h1: {
2232
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h1",
2233
+ "font-size": "22.5pt",
2234
+ "font-weight": "700"
2235
+ },
2236
+ h2: {
2237
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h2",
2238
+ "font-size": "18pt",
2239
+ "font-weight": "700"
2240
+ },
2241
+ h3: {
2242
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h3",
2243
+ "font-size": "15pt",
2244
+ "font-weight": "600"
2245
+ },
2246
+ h4: {
2247
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h4",
2248
+ "font-size": "13.5pt",
2249
+ "font-weight": "600"
2250
+ },
2251
+ h5: {
2252
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h5",
2253
+ "font-size": "12pt",
2254
+ "font-weight": "500"
2255
+ },
2256
+ h6: {
2257
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h6",
2258
+ "font-size": "11.25pt",
2259
+ "font-weight": "500"
2260
+ }
2261
+ };
2262
+ function escapeStyleProperty(property) {
2263
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2264
+ return escapedProperty;
2164
2265
  }
2165
- function getActiveBlockType(editor) {
2166
- let blockType = "paragraph";
2167
- editor.getEditorState().read(() => {
2168
- const selection = $getSelection();
2169
- if (!$isRangeSelection(selection)) {
2170
- return;
2171
- }
2172
- const anchorNode = selection.anchor.getNode();
2173
- const topLevelElement = anchorNode.getTopLevelElementOrThrow();
2174
- if ($isHeadingNode(topLevelElement)) {
2175
- blockType = topLevelElement.getTag();
2176
- }
2177
- });
2178
- return blockType;
2266
+ function stripStyleDeclaration(existingStyle, property) {
2267
+ const escapedProperty = escapeStyleProperty(property);
2268
+ return existingStyle.replace(
2269
+ new RegExp(`${escapedProperty}:\\s*[^;]+;?\\s*`, "g"),
2270
+ ""
2271
+ ).trim();
2272
+ }
2273
+ function isSupportedInlineBlockType(value) {
2274
+ return value === "paragraph" || value === "h1" || value === "h2" || value === "h3" || value === "h4" || value === "h5" || value === "h6";
2179
2275
  }
2180
2276
  function extractStyleValue(style, property) {
2181
- const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2277
+ const escapedProperty = escapeStyleProperty(property);
2182
2278
  const match = style.match(new RegExp(`${escapedProperty}:\\s*([^;]+)`));
2183
2279
  return match ? match[1].trim().replace(/['"]/g, "") : void 0;
2184
2280
  }
2281
+ function removeStyleDeclaration(existingStyle, property) {
2282
+ return stripStyleDeclaration(existingStyle, property);
2283
+ }
2185
2284
  function mergeStyleDeclaration(existingStyle, property, value) {
2186
- const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2187
- const stripped = existingStyle.replace(
2188
- new RegExp(`${escapedProperty}:\\s*[^;]+;?\\s*`, "g"),
2189
- ""
2190
- ).trim();
2285
+ const stripped = stripStyleDeclaration(existingStyle, property);
2191
2286
  const declaration = `${property}: ${value}`;
2192
2287
  return stripped ? `${stripped}; ${declaration}` : declaration;
2193
2288
  }
2289
+ function mergeStyleDeclarations(existingStyle, declarations) {
2290
+ return Object.entries(declarations).reduce(
2291
+ (style, [property, value]) => mergeStyleDeclaration(style, property, value),
2292
+ existingStyle
2293
+ );
2294
+ }
2194
2295
  function extractFontFamilyFromStyle(style) {
2195
2296
  return extractStyleValue(style, "font-family");
2196
2297
  }
@@ -2208,6 +2309,21 @@ function mergeFontFamilyStyle(existingStyle, fontFamily) {
2208
2309
  function mergeFontSizeStyle(existingStyle, size) {
2209
2310
  return mergeStyleDeclaration(existingStyle, "font-size", `${size}pt`);
2210
2311
  }
2312
+ function extractInlineBlockTypeFromStyle(style) {
2313
+ const value = extractStyleValue(style, INLINE_BLOCK_STYLE_PROPERTY);
2314
+ return isSupportedInlineBlockType(value) ? value : void 0;
2315
+ }
2316
+ function createInlineBlockTypeStylePatch(blockType) {
2317
+ return INLINE_BLOCK_STYLE_PRESETS[blockType];
2318
+ }
2319
+ function mergeInlineBlockTypeStyle(existingStyle, blockType) {
2320
+ const baseStyle = [
2321
+ INLINE_BLOCK_STYLE_PROPERTY,
2322
+ "font-size",
2323
+ "font-weight"
2324
+ ].reduce((style, property) => removeStyleDeclaration(style, property), existingStyle);
2325
+ return mergeStyleDeclarations(baseStyle, createInlineBlockTypeStylePatch(blockType));
2326
+ }
2211
2327
  const EMPTY_CONTEXT = {
2212
2328
  definitions: [],
2213
2329
  refreshDefinitions: () => {
@@ -2349,9 +2465,11 @@ function VariableChip({
2349
2465
  const style = useMemo(() => {
2350
2466
  const fontFamily = extractFontFamilyFromStyle(styleValue);
2351
2467
  const fontSize = extractFontSizePtFromStyle(styleValue);
2468
+ const fontWeight = extractStyleValue(styleValue, "font-weight");
2352
2469
  return {
2353
2470
  ...fontFamily ? { fontFamily } : {},
2354
- ...fontSize ? { fontSize: `${fontSize}pt` } : {}
2471
+ ...fontSize ? { fontSize: `${fontSize}pt` } : {},
2472
+ ...fontWeight ? { fontWeight } : {}
2355
2473
  };
2356
2474
  }, [styleValue]);
2357
2475
  const className = [
@@ -2468,7 +2586,126 @@ function $createVariableNode(variableKey, format = 0, style = "") {
2468
2586
  function $isVariableNode(node) {
2469
2587
  return node instanceof VariableNode;
2470
2588
  }
2471
- const FORMAT_MASKS = {
2589
+ function getElementBlockType$1(element) {
2590
+ if ($isHeadingNode(element)) {
2591
+ return element.getTag();
2592
+ }
2593
+ return "paragraph";
2594
+ }
2595
+ function getVariableTopLevelElement(variable) {
2596
+ const topLevelElement = variable.getTopLevelElementOrThrow();
2597
+ return $isElementNode(topLevelElement) ? topLevelElement : null;
2598
+ }
2599
+ function replaceTopLevelBlockType(element, blockType) {
2600
+ const currentType = getElementBlockType$1(element);
2601
+ if (currentType === blockType) {
2602
+ return;
2603
+ }
2604
+ const nextElement = blockType === "paragraph" ? $createParagraphNode() : $createHeadingNode(blockType);
2605
+ nextElement.setFormat(element.getFormatType());
2606
+ nextElement.setIndent(element.getIndent());
2607
+ const children = element.getChildren();
2608
+ for (const child of children) {
2609
+ nextElement.append(child);
2610
+ }
2611
+ element.replace(nextElement);
2612
+ }
2613
+ function isPartialSingleBlockSelection(selection) {
2614
+ if (selection.isCollapsed()) {
2615
+ return false;
2616
+ }
2617
+ const anchorTopLevel = selection.anchor.getNode().getTopLevelElementOrThrow();
2618
+ const focusTopLevel = selection.focus.getNode().getTopLevelElementOrThrow();
2619
+ if (!anchorTopLevel.is(focusTopLevel)) {
2620
+ return false;
2621
+ }
2622
+ const selectedText = selection.getTextContent().trim();
2623
+ const blockText = anchorTopLevel.getTextContent().trim();
2624
+ return selectedText.length > 0 && selectedText.length < blockText.length;
2625
+ }
2626
+ function applySemanticBlockType(selection, blockType) {
2627
+ if (blockType === "paragraph") {
2628
+ $setBlocksType(selection, () => $createParagraphNode());
2629
+ return;
2630
+ }
2631
+ $setBlocksType(selection, () => $createHeadingNode(blockType));
2632
+ }
2633
+ function selectedVariablesOccupyEntireBlock(variables, topLevelElement) {
2634
+ const selectedKeys = new Set(variables.map((variable) => variable.getKey()));
2635
+ const meaningfulChildren = topLevelElement.getChildren().filter(
2636
+ (child) => !($isTextNode(child) && child.getTextContent().trim() === "")
2637
+ );
2638
+ if (meaningfulChildren.length === 0) {
2639
+ return false;
2640
+ }
2641
+ return meaningfulChildren.every(
2642
+ (child) => $isVariableNode(child) && selectedKeys.has(child.getKey())
2643
+ );
2644
+ }
2645
+ function getStandaloneVariableChildren(topLevelElement) {
2646
+ const meaningfulChildren = topLevelElement.getChildren().filter(
2647
+ (child) => !($isTextNode(child) && child.getTextContent().trim() === "")
2648
+ );
2649
+ if (meaningfulChildren.length === 0 || !meaningfulChildren.every($isVariableNode)) {
2650
+ return null;
2651
+ }
2652
+ return meaningfulChildren;
2653
+ }
2654
+ function setBlockType(editor, blockType) {
2655
+ editor.update(() => {
2656
+ const currentSelection = $getSelection();
2657
+ const selection = $isNodeSelection(currentSelection) ? currentSelection : $createRangeSelectionFromDom(window.getSelection(), editor) ?? currentSelection;
2658
+ if ($isRangeSelection(selection)) {
2659
+ $setSelection(selection);
2660
+ }
2661
+ if ($isNodeSelection(selection)) {
2662
+ const variables = selection.getNodes().filter($isVariableNode);
2663
+ if (variables.length === 0) {
2664
+ return;
2665
+ }
2666
+ const firstTopLevelElement = getVariableTopLevelElement(variables[0]);
2667
+ if (!firstTopLevelElement) {
2668
+ return;
2669
+ }
2670
+ const sameTopLevelElement = variables.every(
2671
+ (variable) => {
2672
+ var _a;
2673
+ return ((_a = getVariableTopLevelElement(variable)) == null ? void 0 : _a.is(firstTopLevelElement)) ?? false;
2674
+ }
2675
+ );
2676
+ if (sameTopLevelElement && selectedVariablesOccupyEntireBlock(variables, firstTopLevelElement)) {
2677
+ for (const variable of variables) {
2678
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2679
+ }
2680
+ replaceTopLevelBlockType(firstTopLevelElement, blockType);
2681
+ const nextSelection = $createNodeSelection();
2682
+ for (const variable of variables) {
2683
+ nextSelection.add(variable.getKey());
2684
+ }
2685
+ $setSelection(nextSelection);
2686
+ return;
2687
+ }
2688
+ for (const variable of variables) {
2689
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2690
+ }
2691
+ return;
2692
+ }
2693
+ if (!$isRangeSelection(selection)) {
2694
+ return;
2695
+ }
2696
+ const anchorTopLevel = selection.anchor.getNode().getTopLevelElementOrThrow();
2697
+ const standaloneVariables = $isElementNode(anchorTopLevel) ? getStandaloneVariableChildren(anchorTopLevel) : null;
2698
+ if (isPartialSingleBlockSelection(selection)) {
2699
+ $patchStyleText(selection, createInlineBlockTypeStylePatch(blockType));
2700
+ return;
2701
+ }
2702
+ for (const variable of standaloneVariables ?? []) {
2703
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2704
+ }
2705
+ applySemanticBlockType(selection, blockType);
2706
+ });
2707
+ }
2708
+ const FORMAT_MASKS$1 = {
2472
2709
  bold: 1,
2473
2710
  italic: 2,
2474
2711
  strikethrough: 4,
@@ -2490,19 +2727,8 @@ function withSelectedVariableNodes(editor, updater) {
2490
2727
  });
2491
2728
  return updated;
2492
2729
  }
2493
- function getSelectedVariableNodes(editor) {
2494
- let nodes = [];
2495
- editor.getEditorState().read(() => {
2496
- const selection = $getSelection();
2497
- if (!$isNodeSelection(selection)) {
2498
- return;
2499
- }
2500
- nodes = selection.getNodes().filter($isVariableNode);
2501
- });
2502
- return nodes;
2503
- }
2504
2730
  function toggleSelectedVariableFormat(editor, format) {
2505
- const mask = FORMAT_MASKS[format];
2731
+ const mask = FORMAT_MASKS$1[format];
2506
2732
  if (!mask) {
2507
2733
  return false;
2508
2734
  }
@@ -2528,25 +2754,6 @@ function applyFontSizeToSelectedVariables(editor, size) {
2528
2754
  }
2529
2755
  });
2530
2756
  }
2531
- function readSelectedVariableFormatting(editor) {
2532
- let formatting = {};
2533
- editor.getEditorState().read(() => {
2534
- const selection = $getSelection();
2535
- if (!$isNodeSelection(selection)) {
2536
- return;
2537
- }
2538
- const firstNode = selection.getNodes().filter($isVariableNode)[0];
2539
- if (!firstNode) {
2540
- return;
2541
- }
2542
- const style = firstNode.getStyle();
2543
- formatting = {
2544
- fontFamily: extractFontFamilyFromStyle(style),
2545
- fontSize: extractFontSizePtFromStyle(style)
2546
- };
2547
- });
2548
- return formatting;
2549
- }
2550
2757
  const BLOCK_TYPE_OPTIONS = [
2551
2758
  { value: "paragraph", shortLabel: "P" },
2552
2759
  { value: "h1", shortLabel: "H1" },
@@ -2962,6 +3169,108 @@ const CanvasControls = () => {
2962
3169
  )
2963
3170
  ] });
2964
3171
  };
3172
+ const FORMAT_MASKS = {
3173
+ bold: 1,
3174
+ italic: 2,
3175
+ strikethrough: 4,
3176
+ underline: 8
3177
+ };
3178
+ function normalizeFontFamily(fontFamily) {
3179
+ if (fontFamily && SUPPORTED_FONTS.includes(fontFamily)) {
3180
+ return fontFamily;
3181
+ }
3182
+ return "Calibri";
3183
+ }
3184
+ function normalizeAlignment(alignment) {
3185
+ if (alignment === "center" || alignment === "right" || alignment === "justify") {
3186
+ return alignment;
3187
+ }
3188
+ return "left";
3189
+ }
3190
+ function getElementBlockType(node) {
3191
+ const topLevelElement = node.getTopLevelElementOrThrow();
3192
+ if ($isHeadingNode(topLevelElement)) {
3193
+ return topLevelElement.getTag();
3194
+ }
3195
+ return "paragraph";
3196
+ }
3197
+ function getElementAlignment(node) {
3198
+ const topLevelElement = node.getTopLevelElementOrThrow();
3199
+ if ($isElementNode(topLevelElement)) {
3200
+ return normalizeAlignment(topLevelElement.getFormatType());
3201
+ }
3202
+ return "left";
3203
+ }
3204
+ function getInlineStyleTarget(nodes, anchorNode) {
3205
+ if ($isTextNode(anchorNode) || $isVariableNode(anchorNode)) {
3206
+ return anchorNode;
3207
+ }
3208
+ return nodes.find((node) => $isTextNode(node) || $isVariableNode(node)) ?? null;
3209
+ }
3210
+ function getInlineStyleFromNode(node) {
3211
+ if ($isTextNode(node) || $isVariableNode(node)) {
3212
+ return node.getStyle();
3213
+ }
3214
+ return "";
3215
+ }
3216
+ function hasInlineFormat(node, format) {
3217
+ if ($isVariableNode(node)) {
3218
+ return (node.getFormat() & FORMAT_MASKS[format]) !== 0;
3219
+ }
3220
+ if ($isTextNode(node) && "hasFormat" in node && typeof node.hasFormat === "function") {
3221
+ return node.hasFormat(format);
3222
+ }
3223
+ if ($isTextNode(node) && "getFormat" in node && typeof node.getFormat === "function") {
3224
+ return (node.getFormat() & FORMAT_MASKS[format]) !== 0;
3225
+ }
3226
+ return false;
3227
+ }
3228
+ function readToolbarStyleSnapshot(editor, editorState = editor.getEditorState()) {
3229
+ let snapshot = DEFAULT_TOOLBAR_STYLE_SNAPSHOT;
3230
+ editorState.read(() => {
3231
+ const currentSelection = $getSelection();
3232
+ const selection = $isNodeSelection(currentSelection) ? currentSelection : $createRangeSelectionFromDom(window.getSelection(), editor) ?? currentSelection;
3233
+ if ($isNodeSelection(selection)) {
3234
+ const variableNodes = selection.getNodes().filter($isVariableNode);
3235
+ if (variableNodes.length === 0) {
3236
+ return;
3237
+ }
3238
+ const firstVariableNode = variableNodes[0];
3239
+ const style2 = firstVariableNode.getStyle();
3240
+ snapshot = {
3241
+ blockType: extractInlineBlockTypeFromStyle(style2) ?? getElementBlockType(firstVariableNode),
3242
+ fontFamily: normalizeFontFamily(extractFontFamilyFromStyle(style2)),
3243
+ fontSize: extractFontSizePtFromStyle(style2) ?? DEFAULT_FONT_SIZE,
3244
+ alignment: getElementAlignment(firstVariableNode),
3245
+ isBold: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.bold) !== 0),
3246
+ isItalic: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.italic) !== 0),
3247
+ isUnderline: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.underline) !== 0),
3248
+ isStrikethrough: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.strikethrough) !== 0),
3249
+ hasSelectedVariable: true
3250
+ };
3251
+ return;
3252
+ }
3253
+ if (!$isRangeSelection(selection)) {
3254
+ return;
3255
+ }
3256
+ const anchorNode = selection.anchor.getNode();
3257
+ const inlineStyleTarget = getInlineStyleTarget(selection.getNodes(), anchorNode);
3258
+ const style = selection.style || getInlineStyleFromNode(inlineStyleTarget);
3259
+ const isCollapsed = selection.isCollapsed();
3260
+ snapshot = {
3261
+ blockType: extractInlineBlockTypeFromStyle(style) ?? getElementBlockType(anchorNode),
3262
+ fontFamily: normalizeFontFamily(extractFontFamilyFromStyle(style)),
3263
+ fontSize: extractFontSizePtFromStyle(style) ?? DEFAULT_FONT_SIZE,
3264
+ alignment: getElementAlignment(anchorNode),
3265
+ isBold: selection.hasFormat("bold") || isCollapsed && hasInlineFormat(inlineStyleTarget, "bold"),
3266
+ isItalic: selection.hasFormat("italic") || isCollapsed && hasInlineFormat(inlineStyleTarget, "italic"),
3267
+ isUnderline: selection.hasFormat("underline") || isCollapsed && hasInlineFormat(inlineStyleTarget, "underline"),
3268
+ isStrikethrough: selection.hasFormat("strikethrough") || isCollapsed && hasInlineFormat(inlineStyleTarget, "strikethrough"),
3269
+ hasSelectedVariable: false
3270
+ };
3271
+ });
3272
+ return snapshot;
3273
+ }
2965
3274
  const Toolbar = () => {
2966
3275
  const {
2967
3276
  activeEditor,
@@ -2978,15 +3287,15 @@ const Toolbar = () => {
2978
3287
  const { toolbarItems, toolbarEndItems } = useExtensions();
2979
3288
  const toolbarConfig = useToolbarConfig();
2980
3289
  const t = useTranslations();
2981
- const [activeBlockType, setActiveBlockType] = useState("paragraph");
2982
- const [activeFontFamily, setActiveFontFamily] = useState("Calibri");
2983
- const [activeFontSize, setActiveFontSize] = useState(DEFAULT_FONT_SIZE);
2984
- const normalizeFontFamily = useCallback((fontFamily) => {
2985
- if (fontFamily && SUPPORTED_FONTS.includes(fontFamily)) {
2986
- return fontFamily;
2987
- }
2988
- return "Calibri";
2989
- }, []);
3290
+ const toolbarStyleStore = useToolbarStyleStoreApi();
3291
+ const activeBlockType = useToolbarStyleStore((state) => state.blockType);
3292
+ const activeFontFamily = useToolbarStyleStore((state) => state.fontFamily);
3293
+ const activeFontSize = useToolbarStyleStore((state) => state.fontSize);
3294
+ const activeAlignment = useToolbarStyleStore((state) => state.alignment);
3295
+ const isBoldActive = useToolbarStyleStore((state) => state.isBold);
3296
+ const isItalicActive = useToolbarStyleStore((state) => state.isItalic);
3297
+ const isUnderlineActive = useToolbarStyleStore((state) => state.isUnderline);
3298
+ const isStrikethroughActive = useToolbarStyleStore((state) => state.isStrikethrough);
2990
3299
  const withBodySelection = useCallback(
2991
3300
  (editor, action) => {
2992
3301
  editor.update(() => {
@@ -3024,38 +3333,11 @@ const Toolbar = () => {
3024
3333
  );
3025
3334
  useEffect(() => {
3026
3335
  if (!activeEditor) {
3027
- setActiveBlockType("paragraph");
3028
- setActiveFontFamily("Calibri");
3029
- setActiveFontSize(DEFAULT_FONT_SIZE);
3336
+ toolbarStyleStore.getState().reset();
3030
3337
  return;
3031
3338
  }
3032
- const updateSelectionState = () => {
3033
- const selectedVariables = getSelectedVariableNodes(activeEditor);
3034
- if (selectedVariables.length > 0) {
3035
- const formatting = readSelectedVariableFormatting(activeEditor);
3036
- setActiveBlockType("paragraph");
3037
- setActiveFontFamily(normalizeFontFamily(formatting.fontFamily));
3038
- setActiveFontSize(formatting.fontSize ?? DEFAULT_FONT_SIZE);
3039
- return;
3040
- }
3041
- setActiveBlockType(getActiveBlockType(activeEditor));
3042
- let nextFontFamily = "Calibri";
3043
- let nextFontSize = DEFAULT_FONT_SIZE;
3044
- activeEditor.getEditorState().read(() => {
3045
- const selection = $getSelection();
3046
- if (!$isRangeSelection(selection)) {
3047
- return;
3048
- }
3049
- const textNode = selection.getNodes().find($isTextNode);
3050
- if (!textNode) {
3051
- return;
3052
- }
3053
- const style = textNode.getStyle();
3054
- nextFontFamily = normalizeFontFamily(extractFontFamilyFromStyle(style));
3055
- nextFontSize = extractFontSizePtFromStyle(style) ?? DEFAULT_FONT_SIZE;
3056
- });
3057
- setActiveFontFamily(nextFontFamily);
3058
- setActiveFontSize(nextFontSize);
3339
+ const updateSelectionState = (editorState = activeEditor.getEditorState()) => {
3340
+ toolbarStyleStore.getState().setSnapshot(readToolbarStyleSnapshot(activeEditor, editorState));
3059
3341
  };
3060
3342
  updateSelectionState();
3061
3343
  const unregisterSelectionChange = activeEditor.registerCommand(
@@ -3066,14 +3348,14 @@ const Toolbar = () => {
3066
3348
  },
3067
3349
  COMMAND_PRIORITY_LOW
3068
3350
  );
3069
- const unregisterUpdateListener = activeEditor.registerUpdateListener(() => {
3070
- updateSelectionState();
3351
+ const unregisterUpdateListener = activeEditor.registerUpdateListener(({ editorState }) => {
3352
+ updateSelectionState(editorState);
3071
3353
  });
3072
3354
  return () => {
3073
3355
  unregisterSelectionChange();
3074
3356
  unregisterUpdateListener();
3075
3357
  };
3076
- }, [activeEditor, normalizeFontFamily]);
3358
+ }, [activeEditor, toolbarStyleStore]);
3077
3359
  const handleBold = useCallback(() => {
3078
3360
  debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
3079
3361
  runToolbarAction(t.history.actions.boldApplied, () => {
@@ -3258,17 +3540,17 @@ const Toolbar = () => {
3258
3540
  ] }),
3259
3541
  /* @__PURE__ */ jsx(Divider, {}),
3260
3542
  /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group", "data-testid": "format-group", children: [
3261
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.bold, testId: "btn-bold", onClick: handleBold, children: /* @__PURE__ */ jsx(Bold, { size: 15 }) }),
3262
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.italic, testId: "btn-italic", onClick: handleItalic, children: /* @__PURE__ */ jsx(Italic, { size: 15 }) }),
3263
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.underline, testId: "btn-underline", onClick: handleUnderline, children: /* @__PURE__ */ jsx(Underline, { size: 15 }) }),
3264
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.strikethrough, testId: "btn-strike", onClick: handleStrikethrough, children: /* @__PURE__ */ jsx(Strikethrough, { size: 15 }) })
3543
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.bold, testId: "btn-bold", active: isBoldActive, onClick: handleBold, children: /* @__PURE__ */ jsx(Bold, { size: 15 }) }),
3544
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.italic, testId: "btn-italic", active: isItalicActive, onClick: handleItalic, children: /* @__PURE__ */ jsx(Italic, { size: 15 }) }),
3545
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.underline, testId: "btn-underline", active: isUnderlineActive, onClick: handleUnderline, children: /* @__PURE__ */ jsx(Underline, { size: 15 }) }),
3546
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.strikethrough, testId: "btn-strike", active: isStrikethroughActive, onClick: handleStrikethrough, children: /* @__PURE__ */ jsx(Strikethrough, { size: 15 }) })
3265
3547
  ] }),
3266
3548
  /* @__PURE__ */ jsx(Divider, {}),
3267
3549
  /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group", "data-testid": "align-group", children: [
3268
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignLeft, testId: "btn-align-left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsx(TextAlignStart, { size: 15 }) }),
3269
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignCenter, testId: "btn-align-center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsx(TextAlignCenter, { size: 15 }) }),
3270
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignRight, testId: "btn-align-right", onClick: handleAlignRight, children: /* @__PURE__ */ jsx(TextAlignEnd, { size: 15 }) }),
3271
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.justify, testId: "btn-align-justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsx(TextAlignJustify, { size: 15 }) })
3550
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignLeft, testId: "btn-align-left", active: activeAlignment === "left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsx(TextAlignStart, { size: 15 }) }),
3551
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignCenter, testId: "btn-align-center", active: activeAlignment === "center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsx(TextAlignCenter, { size: 15 }) }),
3552
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignRight, testId: "btn-align-right", active: activeAlignment === "right", onClick: handleAlignRight, children: /* @__PURE__ */ jsx(TextAlignEnd, { size: 15 }) }),
3553
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.justify, testId: "btn-align-justify", active: activeAlignment === "justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsx(TextAlignJustify, { size: 15 }) })
3272
3554
  ] }),
3273
3555
  /* @__PURE__ */ jsx(Divider, {}),
3274
3556
  /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group", "data-testid": "list-group", children: [
@@ -3319,6 +3601,7 @@ const ToolbarIconButton = ({
3319
3601
  type: "button",
3320
3602
  title,
3321
3603
  "aria-label": title,
3604
+ "aria-pressed": active,
3322
3605
  disabled,
3323
3606
  onMouseDown: (e) => e.preventDefault(),
3324
3607
  onClick,