ninegrid2 6.1315.0 → 6.1317.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.
@@ -47627,6 +47627,581 @@ function wrappingInputRule(config) {
47627
47627
  undoable: config.undoable
47628
47628
  });
47629
47629
  }
47630
+
47631
+ // src/lib/ResizableNodeView.ts
47632
+ var isTouchEvent = (e) => {
47633
+ return "touches" in e;
47634
+ };
47635
+ var ResizableNodeView = class {
47636
+ /**
47637
+ * Creates a new ResizableNodeView instance.
47638
+ *
47639
+ * The constructor sets up the resize handles, applies initial sizing from
47640
+ * node attributes, and configures all resize behavior options.
47641
+ *
47642
+ * @param options - Configuration options for the resizable node view
47643
+ */
47644
+ constructor(options) {
47645
+ /** Active resize handle directions */
47646
+ this.directions = ["bottom-left", "bottom-right", "top-left", "top-right"];
47647
+ /** Minimum allowed dimensions */
47648
+ this.minSize = {
47649
+ height: 8,
47650
+ width: 8
47651
+ };
47652
+ /** Whether to always preserve aspect ratio */
47653
+ this.preserveAspectRatio = false;
47654
+ /** CSS class names for elements */
47655
+ this.classNames = {
47656
+ container: "",
47657
+ wrapper: "",
47658
+ handle: "",
47659
+ resizing: ""
47660
+ };
47661
+ /** Initial width of the element (for aspect ratio calculation) */
47662
+ this.initialWidth = 0;
47663
+ /** Initial height of the element (for aspect ratio calculation) */
47664
+ this.initialHeight = 0;
47665
+ /** Calculated aspect ratio (width / height) */
47666
+ this.aspectRatio = 1;
47667
+ /** Whether a resize operation is currently active */
47668
+ this.isResizing = false;
47669
+ /** The handle currently being dragged */
47670
+ this.activeHandle = null;
47671
+ /** Starting mouse X position when resize began */
47672
+ this.startX = 0;
47673
+ /** Starting mouse Y position when resize began */
47674
+ this.startY = 0;
47675
+ /** Element width when resize began */
47676
+ this.startWidth = 0;
47677
+ /** Element height when resize began */
47678
+ this.startHeight = 0;
47679
+ /** Whether Shift key is currently pressed (for temporary aspect ratio lock) */
47680
+ this.isShiftKeyPressed = false;
47681
+ /** Last known editable state of the editor */
47682
+ this.lastEditableState = void 0;
47683
+ /** Map of handle elements by direction */
47684
+ this.handleMap = /* @__PURE__ */ new Map();
47685
+ /**
47686
+ * Handles mouse movement during an active resize.
47687
+ *
47688
+ * Calculates the delta from the starting position, computes new dimensions
47689
+ * based on the active handle direction, applies constraints and aspect ratio,
47690
+ * then updates the element's style and calls the onResize callback.
47691
+ *
47692
+ * @param event - The mouse move event
47693
+ */
47694
+ this.handleMouseMove = (event) => {
47695
+ if (!this.isResizing || !this.activeHandle) {
47696
+ return;
47697
+ }
47698
+ const deltaX = event.clientX - this.startX;
47699
+ const deltaY = event.clientY - this.startY;
47700
+ this.handleResize(deltaX, deltaY);
47701
+ };
47702
+ this.handleTouchMove = (event) => {
47703
+ if (!this.isResizing || !this.activeHandle) {
47704
+ return;
47705
+ }
47706
+ const touch = event.touches[0];
47707
+ if (!touch) {
47708
+ return;
47709
+ }
47710
+ const deltaX = touch.clientX - this.startX;
47711
+ const deltaY = touch.clientY - this.startY;
47712
+ this.handleResize(deltaX, deltaY);
47713
+ };
47714
+ /**
47715
+ * Completes the resize operation when the mouse button is released.
47716
+ *
47717
+ * Captures final dimensions, calls the onCommit callback to persist changes,
47718
+ * removes the resizing state and class, and cleans up document-level listeners.
47719
+ */
47720
+ this.handleMouseUp = () => {
47721
+ if (!this.isResizing) {
47722
+ return;
47723
+ }
47724
+ const finalWidth = this.element.offsetWidth;
47725
+ const finalHeight = this.element.offsetHeight;
47726
+ this.onCommit(finalWidth, finalHeight);
47727
+ this.isResizing = false;
47728
+ this.activeHandle = null;
47729
+ this.container.dataset.resizeState = "false";
47730
+ if (this.classNames.resizing) {
47731
+ this.container.classList.remove(this.classNames.resizing);
47732
+ }
47733
+ document.removeEventListener("mousemove", this.handleMouseMove);
47734
+ document.removeEventListener("mouseup", this.handleMouseUp);
47735
+ document.removeEventListener("keydown", this.handleKeyDown);
47736
+ document.removeEventListener("keyup", this.handleKeyUp);
47737
+ };
47738
+ /**
47739
+ * Tracks Shift key state to enable temporary aspect ratio locking.
47740
+ *
47741
+ * When Shift is pressed during resize, aspect ratio is preserved even if
47742
+ * preserveAspectRatio is false.
47743
+ *
47744
+ * @param event - The keyboard event
47745
+ */
47746
+ this.handleKeyDown = (event) => {
47747
+ if (event.key === "Shift") {
47748
+ this.isShiftKeyPressed = true;
47749
+ }
47750
+ };
47751
+ /**
47752
+ * Tracks Shift key release to disable temporary aspect ratio locking.
47753
+ *
47754
+ * @param event - The keyboard event
47755
+ */
47756
+ this.handleKeyUp = (event) => {
47757
+ if (event.key === "Shift") {
47758
+ this.isShiftKeyPressed = false;
47759
+ }
47760
+ };
47761
+ var _a, _b, _c, _d, _e, _f;
47762
+ this.node = options.node;
47763
+ this.editor = options.editor;
47764
+ this.element = options.element;
47765
+ this.contentElement = options.contentElement;
47766
+ this.getPos = options.getPos;
47767
+ this.onResize = options.onResize;
47768
+ this.onCommit = options.onCommit;
47769
+ this.onUpdate = options.onUpdate;
47770
+ if ((_a = options.options) == null ? void 0 : _a.min) {
47771
+ this.minSize = {
47772
+ ...this.minSize,
47773
+ ...options.options.min
47774
+ };
47775
+ }
47776
+ if ((_b = options.options) == null ? void 0 : _b.max) {
47777
+ this.maxSize = options.options.max;
47778
+ }
47779
+ if ((_c = options == null ? void 0 : options.options) == null ? void 0 : _c.directions) {
47780
+ this.directions = options.options.directions;
47781
+ }
47782
+ if ((_d = options.options) == null ? void 0 : _d.preserveAspectRatio) {
47783
+ this.preserveAspectRatio = options.options.preserveAspectRatio;
47784
+ }
47785
+ if ((_e = options.options) == null ? void 0 : _e.className) {
47786
+ this.classNames = {
47787
+ container: options.options.className.container || "",
47788
+ wrapper: options.options.className.wrapper || "",
47789
+ handle: options.options.className.handle || "",
47790
+ resizing: options.options.className.resizing || ""
47791
+ };
47792
+ }
47793
+ if ((_f = options.options) == null ? void 0 : _f.createCustomHandle) {
47794
+ this.createCustomHandle = options.options.createCustomHandle;
47795
+ }
47796
+ this.wrapper = this.createWrapper();
47797
+ this.container = this.createContainer();
47798
+ this.applyInitialSize();
47799
+ this.attachHandles();
47800
+ this.editor.on("update", this.handleEditorUpdate.bind(this));
47801
+ }
47802
+ /**
47803
+ * Returns the top-level DOM node that should be placed in the editor.
47804
+ *
47805
+ * This is required by the ProseMirror NodeView interface. The container
47806
+ * includes the wrapper, handles, and the actual content element.
47807
+ *
47808
+ * @returns The container element to be inserted into the editor
47809
+ */
47810
+ get dom() {
47811
+ return this.container;
47812
+ }
47813
+ get contentDOM() {
47814
+ var _a;
47815
+ return (_a = this.contentElement) != null ? _a : null;
47816
+ }
47817
+ handleEditorUpdate() {
47818
+ const isEditable = this.editor.isEditable;
47819
+ if (isEditable === this.lastEditableState) {
47820
+ return;
47821
+ }
47822
+ this.lastEditableState = isEditable;
47823
+ if (!isEditable) {
47824
+ this.removeHandles();
47825
+ } else if (isEditable && this.handleMap.size === 0) {
47826
+ this.attachHandles();
47827
+ }
47828
+ }
47829
+ /**
47830
+ * Called when the node's content or attributes change.
47831
+ *
47832
+ * Updates the internal node reference. If a custom `onUpdate` callback
47833
+ * was provided, it will be called to handle additional update logic.
47834
+ *
47835
+ * @param node - The new/updated node
47836
+ * @param decorations - Node decorations
47837
+ * @param innerDecorations - Inner decorations
47838
+ * @returns `false` if the node type has changed (requires full rebuild), otherwise the result of `onUpdate` or `true`
47839
+ */
47840
+ update(node, decorations, innerDecorations) {
47841
+ if (node.type !== this.node.type) {
47842
+ return false;
47843
+ }
47844
+ this.node = node;
47845
+ if (this.onUpdate) {
47846
+ return this.onUpdate(node, decorations, innerDecorations);
47847
+ }
47848
+ return true;
47849
+ }
47850
+ /**
47851
+ * Cleanup method called when the node view is being removed.
47852
+ *
47853
+ * Removes all event listeners to prevent memory leaks. This is required
47854
+ * by the ProseMirror NodeView interface. If a resize is active when
47855
+ * destroy is called, it will be properly cancelled.
47856
+ */
47857
+ destroy() {
47858
+ if (this.isResizing) {
47859
+ this.container.dataset.resizeState = "false";
47860
+ if (this.classNames.resizing) {
47861
+ this.container.classList.remove(this.classNames.resizing);
47862
+ }
47863
+ document.removeEventListener("mousemove", this.handleMouseMove);
47864
+ document.removeEventListener("mouseup", this.handleMouseUp);
47865
+ document.removeEventListener("keydown", this.handleKeyDown);
47866
+ document.removeEventListener("keyup", this.handleKeyUp);
47867
+ this.isResizing = false;
47868
+ this.activeHandle = null;
47869
+ }
47870
+ this.editor.off("update", this.handleEditorUpdate.bind(this));
47871
+ this.container.remove();
47872
+ }
47873
+ /**
47874
+ * Creates the outer container element.
47875
+ *
47876
+ * The container is the top-level element returned by the NodeView and
47877
+ * wraps the entire resizable node. It's set up with flexbox to handle
47878
+ * alignment and includes data attributes for styling and identification.
47879
+ *
47880
+ * @returns The container element
47881
+ */
47882
+ createContainer() {
47883
+ const element = document.createElement("div");
47884
+ element.dataset.resizeContainer = "";
47885
+ element.dataset.node = this.node.type.name;
47886
+ element.style.display = "flex";
47887
+ if (this.classNames.container) {
47888
+ element.className = this.classNames.container;
47889
+ }
47890
+ element.appendChild(this.wrapper);
47891
+ return element;
47892
+ }
47893
+ /**
47894
+ * Creates the wrapper element that contains the content and handles.
47895
+ *
47896
+ * The wrapper uses relative positioning so that resize handles can be
47897
+ * positioned absolutely within it. This is the direct parent of the
47898
+ * content element being made resizable.
47899
+ *
47900
+ * @returns The wrapper element
47901
+ */
47902
+ createWrapper() {
47903
+ const element = document.createElement("div");
47904
+ element.style.position = "relative";
47905
+ element.style.display = "block";
47906
+ element.dataset.resizeWrapper = "";
47907
+ if (this.classNames.wrapper) {
47908
+ element.className = this.classNames.wrapper;
47909
+ }
47910
+ element.appendChild(this.element);
47911
+ return element;
47912
+ }
47913
+ /**
47914
+ * Creates a resize handle element for a specific direction.
47915
+ *
47916
+ * Each handle is absolutely positioned and includes a data attribute
47917
+ * identifying its direction for styling purposes.
47918
+ *
47919
+ * @param direction - The resize direction for this handle
47920
+ * @returns The handle element
47921
+ */
47922
+ createHandle(direction) {
47923
+ const handle = document.createElement("div");
47924
+ handle.dataset.resizeHandle = direction;
47925
+ handle.style.position = "absolute";
47926
+ if (this.classNames.handle) {
47927
+ handle.className = this.classNames.handle;
47928
+ }
47929
+ return handle;
47930
+ }
47931
+ /**
47932
+ * Positions a handle element according to its direction.
47933
+ *
47934
+ * Corner handles (e.g., 'top-left') are positioned at the intersection
47935
+ * of two edges. Edge handles (e.g., 'top') span the full width or height.
47936
+ *
47937
+ * @param handle - The handle element to position
47938
+ * @param direction - The direction determining the position
47939
+ */
47940
+ positionHandle(handle, direction) {
47941
+ const isTop = direction.includes("top");
47942
+ const isBottom = direction.includes("bottom");
47943
+ const isLeft = direction.includes("left");
47944
+ const isRight = direction.includes("right");
47945
+ if (isTop) {
47946
+ handle.style.top = "0";
47947
+ }
47948
+ if (isBottom) {
47949
+ handle.style.bottom = "0";
47950
+ }
47951
+ if (isLeft) {
47952
+ handle.style.left = "0";
47953
+ }
47954
+ if (isRight) {
47955
+ handle.style.right = "0";
47956
+ }
47957
+ if (direction === "top" || direction === "bottom") {
47958
+ handle.style.left = "0";
47959
+ handle.style.right = "0";
47960
+ }
47961
+ if (direction === "left" || direction === "right") {
47962
+ handle.style.top = "0";
47963
+ handle.style.bottom = "0";
47964
+ }
47965
+ }
47966
+ /**
47967
+ * Creates and attaches all resize handles to the wrapper.
47968
+ *
47969
+ * Iterates through the configured directions, creates a handle for each,
47970
+ * positions it, attaches the mousedown listener, and appends it to the DOM.
47971
+ */
47972
+ attachHandles() {
47973
+ this.directions.forEach((direction) => {
47974
+ let handle;
47975
+ if (this.createCustomHandle) {
47976
+ handle = this.createCustomHandle(direction);
47977
+ } else {
47978
+ handle = this.createHandle(direction);
47979
+ }
47980
+ if (!(handle instanceof HTMLElement)) {
47981
+ console.warn(
47982
+ `[ResizableNodeView] createCustomHandle("${direction}") did not return an HTMLElement. Falling back to default handle.`
47983
+ );
47984
+ handle = this.createHandle(direction);
47985
+ }
47986
+ if (!this.createCustomHandle) {
47987
+ this.positionHandle(handle, direction);
47988
+ }
47989
+ handle.addEventListener("mousedown", (event) => this.handleResizeStart(event, direction));
47990
+ handle.addEventListener("touchstart", (event) => this.handleResizeStart(event, direction));
47991
+ this.handleMap.set(direction, handle);
47992
+ this.wrapper.appendChild(handle);
47993
+ });
47994
+ }
47995
+ /**
47996
+ * Removes all resize handles from the wrapper.
47997
+ *
47998
+ * Cleans up the handle map and removes each handle element from the DOM.
47999
+ */
48000
+ removeHandles() {
48001
+ this.handleMap.forEach((el) => el.remove());
48002
+ this.handleMap.clear();
48003
+ }
48004
+ /**
48005
+ * Applies initial sizing from node attributes to the element.
48006
+ *
48007
+ * If width/height attributes exist on the node, they're applied to the element.
48008
+ * Otherwise, the element's natural/current dimensions are measured. The aspect
48009
+ * ratio is calculated for later use in aspect-ratio-preserving resizes.
48010
+ */
48011
+ applyInitialSize() {
48012
+ const width = this.node.attrs.width;
48013
+ const height = this.node.attrs.height;
48014
+ if (width) {
48015
+ this.element.style.width = `${width}px`;
48016
+ this.initialWidth = width;
48017
+ } else {
48018
+ this.initialWidth = this.element.offsetWidth;
48019
+ }
48020
+ if (height) {
48021
+ this.element.style.height = `${height}px`;
48022
+ this.initialHeight = height;
48023
+ } else {
48024
+ this.initialHeight = this.element.offsetHeight;
48025
+ }
48026
+ if (this.initialWidth > 0 && this.initialHeight > 0) {
48027
+ this.aspectRatio = this.initialWidth / this.initialHeight;
48028
+ }
48029
+ }
48030
+ /**
48031
+ * Initiates a resize operation when a handle is clicked.
48032
+ *
48033
+ * Captures the starting mouse position and element dimensions, sets up
48034
+ * the resize state, adds the resizing class and state attribute, and
48035
+ * attaches document-level listeners for mouse movement and keyboard input.
48036
+ *
48037
+ * @param event - The mouse down event
48038
+ * @param direction - The direction of the handle being dragged
48039
+ */
48040
+ handleResizeStart(event, direction) {
48041
+ event.preventDefault();
48042
+ event.stopPropagation();
48043
+ this.isResizing = true;
48044
+ this.activeHandle = direction;
48045
+ if (isTouchEvent(event)) {
48046
+ this.startX = event.touches[0].clientX;
48047
+ this.startY = event.touches[0].clientY;
48048
+ } else {
48049
+ this.startX = event.clientX;
48050
+ this.startY = event.clientY;
48051
+ }
48052
+ this.startWidth = this.element.offsetWidth;
48053
+ this.startHeight = this.element.offsetHeight;
48054
+ if (this.startWidth > 0 && this.startHeight > 0) {
48055
+ this.aspectRatio = this.startWidth / this.startHeight;
48056
+ }
48057
+ this.getPos();
48058
+ this.container.dataset.resizeState = "true";
48059
+ if (this.classNames.resizing) {
48060
+ this.container.classList.add(this.classNames.resizing);
48061
+ }
48062
+ document.addEventListener("mousemove", this.handleMouseMove);
48063
+ document.addEventListener("touchmove", this.handleTouchMove);
48064
+ document.addEventListener("mouseup", this.handleMouseUp);
48065
+ document.addEventListener("keydown", this.handleKeyDown);
48066
+ document.addEventListener("keyup", this.handleKeyUp);
48067
+ }
48068
+ handleResize(deltaX, deltaY) {
48069
+ if (!this.activeHandle) {
48070
+ return;
48071
+ }
48072
+ const shouldPreserveAspectRatio = this.preserveAspectRatio || this.isShiftKeyPressed;
48073
+ const { width, height } = this.calculateNewDimensions(this.activeHandle, deltaX, deltaY);
48074
+ const constrained = this.applyConstraints(width, height, shouldPreserveAspectRatio);
48075
+ this.element.style.width = `${constrained.width}px`;
48076
+ this.element.style.height = `${constrained.height}px`;
48077
+ if (this.onResize) {
48078
+ this.onResize(constrained.width, constrained.height);
48079
+ }
48080
+ }
48081
+ /**
48082
+ * Calculates new dimensions based on mouse delta and resize direction.
48083
+ *
48084
+ * Takes the starting dimensions and applies the mouse movement delta
48085
+ * according to the handle direction. For corner handles, both dimensions
48086
+ * are affected. For edge handles, only one dimension changes. If aspect
48087
+ * ratio should be preserved, delegates to applyAspectRatio.
48088
+ *
48089
+ * @param direction - The active resize handle direction
48090
+ * @param deltaX - Horizontal mouse movement since resize start
48091
+ * @param deltaY - Vertical mouse movement since resize start
48092
+ * @returns The calculated width and height
48093
+ */
48094
+ calculateNewDimensions(direction, deltaX, deltaY) {
48095
+ let newWidth = this.startWidth;
48096
+ let newHeight = this.startHeight;
48097
+ const isRight = direction.includes("right");
48098
+ const isLeft = direction.includes("left");
48099
+ const isBottom = direction.includes("bottom");
48100
+ const isTop = direction.includes("top");
48101
+ if (isRight) {
48102
+ newWidth = this.startWidth + deltaX;
48103
+ } else if (isLeft) {
48104
+ newWidth = this.startWidth - deltaX;
48105
+ }
48106
+ if (isBottom) {
48107
+ newHeight = this.startHeight + deltaY;
48108
+ } else if (isTop) {
48109
+ newHeight = this.startHeight - deltaY;
48110
+ }
48111
+ if (direction === "right" || direction === "left") {
48112
+ newWidth = this.startWidth + (isRight ? deltaX : -deltaX);
48113
+ }
48114
+ if (direction === "top" || direction === "bottom") {
48115
+ newHeight = this.startHeight + (isBottom ? deltaY : -deltaY);
48116
+ }
48117
+ const shouldPreserveAspectRatio = this.preserveAspectRatio || this.isShiftKeyPressed;
48118
+ if (shouldPreserveAspectRatio) {
48119
+ return this.applyAspectRatio(newWidth, newHeight, direction);
48120
+ }
48121
+ return { width: newWidth, height: newHeight };
48122
+ }
48123
+ /**
48124
+ * Applies min/max constraints to dimensions.
48125
+ *
48126
+ * When aspect ratio is NOT preserved, constraints are applied independently
48127
+ * to width and height. When aspect ratio IS preserved, constraints are
48128
+ * applied while maintaining the aspect ratio—if one dimension hits a limit,
48129
+ * the other is recalculated proportionally.
48130
+ *
48131
+ * This ensures that aspect ratio is never broken when constrained.
48132
+ *
48133
+ * @param width - The unconstrained width
48134
+ * @param height - The unconstrained height
48135
+ * @param preserveAspectRatio - Whether to maintain aspect ratio while constraining
48136
+ * @returns The constrained dimensions
48137
+ */
48138
+ applyConstraints(width, height, preserveAspectRatio) {
48139
+ var _a, _b, _c, _d;
48140
+ if (!preserveAspectRatio) {
48141
+ let constrainedWidth2 = Math.max(this.minSize.width, width);
48142
+ let constrainedHeight2 = Math.max(this.minSize.height, height);
48143
+ if ((_a = this.maxSize) == null ? void 0 : _a.width) {
48144
+ constrainedWidth2 = Math.min(this.maxSize.width, constrainedWidth2);
48145
+ }
48146
+ if ((_b = this.maxSize) == null ? void 0 : _b.height) {
48147
+ constrainedHeight2 = Math.min(this.maxSize.height, constrainedHeight2);
48148
+ }
48149
+ return { width: constrainedWidth2, height: constrainedHeight2 };
48150
+ }
48151
+ let constrainedWidth = width;
48152
+ let constrainedHeight = height;
48153
+ if (constrainedWidth < this.minSize.width) {
48154
+ constrainedWidth = this.minSize.width;
48155
+ constrainedHeight = constrainedWidth / this.aspectRatio;
48156
+ }
48157
+ if (constrainedHeight < this.minSize.height) {
48158
+ constrainedHeight = this.minSize.height;
48159
+ constrainedWidth = constrainedHeight * this.aspectRatio;
48160
+ }
48161
+ if (((_c = this.maxSize) == null ? void 0 : _c.width) && constrainedWidth > this.maxSize.width) {
48162
+ constrainedWidth = this.maxSize.width;
48163
+ constrainedHeight = constrainedWidth / this.aspectRatio;
48164
+ }
48165
+ if (((_d = this.maxSize) == null ? void 0 : _d.height) && constrainedHeight > this.maxSize.height) {
48166
+ constrainedHeight = this.maxSize.height;
48167
+ constrainedWidth = constrainedHeight * this.aspectRatio;
48168
+ }
48169
+ return { width: constrainedWidth, height: constrainedHeight };
48170
+ }
48171
+ /**
48172
+ * Adjusts dimensions to maintain the original aspect ratio.
48173
+ *
48174
+ * For horizontal handles (left/right), uses width as the primary dimension
48175
+ * and calculates height from it. For vertical handles (top/bottom), uses
48176
+ * height as primary and calculates width. For corner handles, uses width
48177
+ * as the primary dimension.
48178
+ *
48179
+ * @param width - The new width
48180
+ * @param height - The new height
48181
+ * @param direction - The active resize direction
48182
+ * @returns Dimensions adjusted to preserve aspect ratio
48183
+ */
48184
+ applyAspectRatio(width, height, direction) {
48185
+ const isHorizontal = direction === "left" || direction === "right";
48186
+ const isVertical = direction === "top" || direction === "bottom";
48187
+ if (isHorizontal) {
48188
+ return {
48189
+ width,
48190
+ height: width / this.aspectRatio
48191
+ };
48192
+ }
48193
+ if (isVertical) {
48194
+ return {
48195
+ width: height * this.aspectRatio,
48196
+ height
48197
+ };
48198
+ }
48199
+ return {
48200
+ width,
48201
+ height: width / this.aspectRatio
48202
+ };
48203
+ }
48204
+ };
47630
48205
  function canInsertNode(state, nodeType) {
47631
48206
  const { selection } = state;
47632
48207
  const { $from } = selection;
@@ -48252,7 +48827,7 @@ var h = (tag, attributes) => {
48252
48827
  };
48253
48828
 
48254
48829
  // src/blockquote.tsx
48255
- var inputRegex$3 = /^\s*>\s$/;
48830
+ var inputRegex$5 = /^\s*>\s$/;
48256
48831
  var Blockquote = Node3.create({
48257
48832
  name: "blockquote",
48258
48833
  addOptions() {
@@ -48314,7 +48889,7 @@ ${prefix}
48314
48889
  addInputRules() {
48315
48890
  return [
48316
48891
  wrappingInputRule({
48317
- find: inputRegex$3,
48892
+ find: inputRegex$5,
48318
48893
  type: this.type
48319
48894
  })
48320
48895
  ];
@@ -48408,8 +48983,8 @@ var Bold = Mark.create({
48408
48983
  });
48409
48984
 
48410
48985
  // src/code.ts
48411
- var inputRegex$2 = /(^|[^`])`([^`]+)`(?!`)$/;
48412
- var pasteRegex$1 = /(^|[^`])`([^`]+)`(?!`)/g;
48986
+ var inputRegex$4 = /(^|[^`])`([^`]+)`(?!`)$/;
48987
+ var pasteRegex$2 = /(^|[^`])`([^`]+)`(?!`)/g;
48413
48988
  var Code = Mark.create({
48414
48989
  name: "code",
48415
48990
  addOptions() {
@@ -48457,7 +49032,7 @@ var Code = Mark.create({
48457
49032
  addInputRules() {
48458
49033
  return [
48459
49034
  markInputRule({
48460
- find: inputRegex$2,
49035
+ find: inputRegex$4,
48461
49036
  type: this.type
48462
49037
  })
48463
49038
  ];
@@ -48465,7 +49040,7 @@ var Code = Mark.create({
48465
49040
  addPasteRules() {
48466
49041
  return [
48467
49042
  markPasteRule({
48468
- find: pasteRegex$1,
49043
+ find: pasteRegex$2,
48469
49044
  type: this.type
48470
49045
  })
48471
49046
  ];
@@ -51960,7 +52535,7 @@ var OrderedList = Node3.create({
51960
52535
  return [inputRule];
51961
52536
  }
51962
52537
  });
51963
- var inputRegex$1 = /^\s*(\[([( |x])?\])\s$/;
52538
+ var inputRegex$3 = /^\s*(\[([( |x])?\])\s$/;
51964
52539
  var TaskItem = Node3.create({
51965
52540
  name: "taskItem",
51966
52541
  addOptions() {
@@ -52147,7 +52722,7 @@ var TaskItem = Node3.create({
52147
52722
  addInputRules() {
52148
52723
  return [
52149
52724
  wrappingInputRule({
52150
- find: inputRegex$1,
52725
+ find: inputRegex$3,
52151
52726
  type: this.type,
52152
52727
  getAttributes: (match) => ({
52153
52728
  checked: match[match.length - 1] === "x"
@@ -52366,8 +52941,8 @@ var Paragraph = Node3.create({
52366
52941
  });
52367
52942
 
52368
52943
  // src/strike.ts
52369
- var inputRegex = /(?:^|\s)(~~(?!\s+~~)((?:[^~]+))~~(?!\s+~~))$/;
52370
- var pasteRegex = /(?:^|\s)(~~(?!\s+~~)((?:[^~]+))~~(?!\s+~~))/g;
52944
+ var inputRegex$2 = /(?:^|\s)(~~(?!\s+~~)((?:[^~]+))~~(?!\s+~~))$/;
52945
+ var pasteRegex$1 = /(?:^|\s)(~~(?!\s+~~)((?:[^~]+))~~(?!\s+~~))/g;
52371
52946
  var Strike = Mark.create({
52372
52947
  name: "strike",
52373
52948
  addOptions() {
@@ -52424,7 +52999,7 @@ var Strike = Mark.create({
52424
52999
  addInputRules() {
52425
53000
  return [
52426
53001
  markInputRule({
52427
- find: inputRegex,
53002
+ find: inputRegex$2,
52428
53003
  type: this.type
52429
53004
  })
52430
53005
  ];
@@ -52432,7 +53007,7 @@ var Strike = Mark.create({
52432
53007
  addPasteRules() {
52433
53008
  return [
52434
53009
  markPasteRule({
52435
- find: pasteRegex,
53010
+ find: pasteRegex$1,
52436
53011
  type: this.type
52437
53012
  })
52438
53013
  ];
@@ -53947,116 +54522,781 @@ var StarterKit = Extension.create({
53947
54522
  // src/index.ts
53948
54523
  var index_default = StarterKit;
53949
54524
 
53950
- //const QUILL_STYLE_URL = "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css";
53951
-
53952
- class nxEditor extends nxDiv {
53953
- #editor = null;
53954
- #quill = null;
53955
- #container = null;
53956
-
53957
- constructor() {
53958
- super();
53959
- }
53960
-
53961
- connectedCallback() {
53962
- if (super.connectedCallback()) {
53963
- this.#render();
53964
- this.#initEditor();
53965
- }
53966
- }
53967
-
53968
- #render() {
53969
- // TipTap은 UI가 없으므로 스타일을 직접 정의해야 합니다.
53970
- this.shadowRoot.innerHTML = `
53971
- <style>
53972
- @import "https://cdn.jsdelivr.net/npm/react-quill-new@latest/dist/quill.snow.css";
53973
- @import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/nxEditor.css";
53974
- ${ninegrid.getCustomPath(this,"nxEditor.css")}
53975
-
53976
- :host { display: block; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }
53977
- .menu-bar { background: #f5f5f5; padding: 8px; border-bottom: 1px solid #ddd; }
53978
- .menu-bar button { margin-right: 4px; cursor: pointer; border: 1px solid #ccc; background: white; }
53979
- .menu-bar button.is-active { background: #333; color: white; }
53980
-
53981
- /* 에디터 기본 스타일 (중요) */
53982
- .tiptap { padding: 15px; min-height: 200px; outline: none; }
53983
- .tiptap p.is-editor-empty:first-child::before {
53984
- content: attr(data-placeholder); float: left; color: #adb5bd; pointer-events: none; height: 0;
53985
- }
53986
- </style>
53987
- <div class="menu-bar">
53988
- <button type="button" id="bold-btn"><b>B</b></button>
53989
- <button type="button" id="italic-btn"><i>I</i></button>
53990
- </div>
53991
- <div id="editor-container"></div>
53992
- `;
53993
- this.#container = this.shadowRoot.querySelector('#editor-container');
53994
- }
53995
-
53996
- #initEditor() {
53997
- this.#editor = new Editor({
53998
- element: this.#container,
53999
- extensions: [
54000
- index_default, // 기본적인 서식(Bold, Italic, List 등) 포함
54001
- ],
54002
- content: this.originContents || '<p>내용을 입력하세요...</p>',
54003
- // Shadow DOM 환경에서 포커스 유실을 방지하는 설정
54004
- injectCSS: true,
54005
- onUpdate: ({ editor }) => {
54006
- // 내용이 바뀔 때마다 외부로 알림 (필요 시)
54007
- const html = editor.getHTML();
54008
- this.dispatchEvent(new CustomEvent('change', { detail: html }));
54009
- }
54010
- });
54011
-
54012
- // 메뉴 버튼 이벤트 연결
54013
- this.shadowRoot.querySelector('#bold-btn').onclick = () =>
54014
- this.#editor.chain().focus().toggleBold().run();
54015
- this.shadowRoot.querySelector('#italic-btn').onclick = () =>
54016
- this.#editor.chain().focus().toggleItalic().run();
54017
- }
54018
-
54019
- //@import "https://cdn.ckeditor.com/ckeditor5/44.1.0/ckeditor5.css";
54020
- #init = async () => {
54021
- const htmlTmpl = document.createElement("template");
54022
- htmlTmpl.innerHTML = `
54023
- <style>
54024
- @import "https://cdn.jsdelivr.net/npm/react-quill-new@latest/dist/quill.snow.css";
54025
- @import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/nxEditor.css";
54026
- ${ninegrid.getCustomPath(this,"nxEditor.css")}
54027
- </style>
54028
- <ReactQuill
54029
- theme="snow"
54030
- value="aaaa"
54031
- //onChange={setContent}
54032
- modules={{
54033
- toolbar: [
54034
- [{ 'header': [1, 2, false] }],
54035
- ['bold', 'italic', 'underline', 'strike'],
54036
- [{ 'list': 'ordered' }, { 'list': 'bullet' }],
54037
- ['link', 'image'], // 이미지 버튼 포함
54038
- ['clean']
54039
- ],
54040
- }}
54041
- //style={{ height: '500px', width: '100%', border: '1px solid #ccc' }}
54042
- />
54043
- `;
54044
- }
54045
-
54046
- setData = (value) => {
54047
- if (this.#editor) {
54048
- this.#editor.setData(value || "");
54049
- } else {
54050
- this.originContents = value;
54051
- }
54052
- }
54053
-
54054
- getData = () => {
54055
- return this.#editor ? this.#editor.getData() : "";
54056
- }
54057
-
54058
- disconnectedCallback() {
54059
- if (this.#editor) ;
54525
+ // src/text-style/index.ts
54526
+ var MAX_FIND_CHILD_SPAN_DEPTH = 20;
54527
+ var findChildSpans = (element, depth = 0) => {
54528
+ const childSpans = [];
54529
+ if (!element.children.length || depth > MAX_FIND_CHILD_SPAN_DEPTH) {
54530
+ return childSpans;
54531
+ }
54532
+ Array.from(element.children).forEach((child) => {
54533
+ if (child.tagName === "SPAN") {
54534
+ childSpans.push(child);
54535
+ } else if (child.children.length) {
54536
+ childSpans.push(...findChildSpans(child, depth + 1));
54537
+ }
54538
+ });
54539
+ return childSpans;
54540
+ };
54541
+ var mergeNestedSpanStyles = (element) => {
54542
+ if (!element.children.length) {
54543
+ return;
54544
+ }
54545
+ const childSpans = findChildSpans(element);
54546
+ if (!childSpans) {
54547
+ return;
54548
+ }
54549
+ childSpans.forEach((childSpan) => {
54550
+ var _a, _b;
54551
+ const childStyle = childSpan.getAttribute("style");
54552
+ const closestParentSpanStyleOfChild = (_b = (_a = childSpan.parentElement) == null ? void 0 : _a.closest("span")) == null ? void 0 : _b.getAttribute("style");
54553
+ childSpan.setAttribute("style", `${closestParentSpanStyleOfChild};${childStyle}`);
54554
+ });
54555
+ };
54556
+ var TextStyle = Mark.create({
54557
+ name: "textStyle",
54558
+ priority: 101,
54559
+ addOptions() {
54560
+ return {
54561
+ HTMLAttributes: {},
54562
+ mergeNestedSpanStyles: true
54563
+ };
54564
+ },
54565
+ parseHTML() {
54566
+ return [
54567
+ {
54568
+ tag: "span",
54569
+ consuming: false,
54570
+ getAttrs: (element) => {
54571
+ const hasStyles = element.hasAttribute("style");
54572
+ if (!hasStyles) {
54573
+ return false;
54574
+ }
54575
+ if (this.options.mergeNestedSpanStyles) {
54576
+ mergeNestedSpanStyles(element);
54577
+ }
54578
+ return {};
54579
+ }
54580
+ }
54581
+ ];
54582
+ },
54583
+ renderHTML({ HTMLAttributes }) {
54584
+ return ["span", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
54585
+ },
54586
+ addCommands() {
54587
+ return {
54588
+ toggleTextStyle: (attributes) => ({ commands }) => {
54589
+ return commands.toggleMark(this.name, attributes);
54590
+ },
54591
+ removeEmptyTextStyle: () => ({ tr }) => {
54592
+ const { selection } = tr;
54593
+ tr.doc.nodesBetween(selection.from, selection.to, (node, pos) => {
54594
+ if (node.isTextblock) {
54595
+ return true;
54596
+ }
54597
+ if (!node.marks.filter((mark) => mark.type === this.type).some((mark) => Object.values(mark.attrs).some((value) => !!value))) {
54598
+ tr.removeMark(pos, pos + node.nodeSize, this.type);
54599
+ }
54600
+ });
54601
+ return true;
54602
+ }
54603
+ };
54604
+ }
54605
+ });
54606
+ var BackgroundColor = Extension.create({
54607
+ name: "backgroundColor",
54608
+ addOptions() {
54609
+ return {
54610
+ types: ["textStyle"]
54611
+ };
54612
+ },
54613
+ addGlobalAttributes() {
54614
+ return [
54615
+ {
54616
+ types: this.options.types,
54617
+ attributes: {
54618
+ backgroundColor: {
54619
+ default: null,
54620
+ parseHTML: (element) => {
54621
+ var _a;
54622
+ const styleAttr = element.getAttribute("style");
54623
+ if (styleAttr) {
54624
+ const decls = styleAttr.split(";").map((s) => s.trim()).filter(Boolean);
54625
+ for (let i = decls.length - 1; i >= 0; i -= 1) {
54626
+ const parts = decls[i].split(":");
54627
+ if (parts.length >= 2) {
54628
+ const prop = parts[0].trim().toLowerCase();
54629
+ const val = parts.slice(1).join(":").trim();
54630
+ if (prop === "background-color") {
54631
+ return val.replace(/['"]+/g, "");
54632
+ }
54633
+ }
54634
+ }
54635
+ }
54636
+ return (_a = element.style.backgroundColor) == null ? void 0 : _a.replace(/['"]+/g, "");
54637
+ },
54638
+ renderHTML: (attributes) => {
54639
+ if (!attributes.backgroundColor) {
54640
+ return {};
54641
+ }
54642
+ return {
54643
+ style: `background-color: ${attributes.backgroundColor}`
54644
+ };
54645
+ }
54646
+ }
54647
+ }
54648
+ }
54649
+ ];
54650
+ },
54651
+ addCommands() {
54652
+ return {
54653
+ setBackgroundColor: (backgroundColor) => ({ chain }) => {
54654
+ return chain().setMark("textStyle", { backgroundColor }).run();
54655
+ },
54656
+ unsetBackgroundColor: () => ({ chain }) => {
54657
+ return chain().setMark("textStyle", { backgroundColor: null }).removeEmptyTextStyle().run();
54658
+ }
54659
+ };
54660
+ }
54661
+ });
54662
+ var Color = Extension.create({
54663
+ name: "color",
54664
+ addOptions() {
54665
+ return {
54666
+ types: ["textStyle"]
54667
+ };
54668
+ },
54669
+ addGlobalAttributes() {
54670
+ return [
54671
+ {
54672
+ types: this.options.types,
54673
+ attributes: {
54674
+ color: {
54675
+ default: null,
54676
+ parseHTML: (element) => {
54677
+ var _a;
54678
+ const styleAttr = element.getAttribute("style");
54679
+ if (styleAttr) {
54680
+ const decls = styleAttr.split(";").map((s) => s.trim()).filter(Boolean);
54681
+ for (let i = decls.length - 1; i >= 0; i -= 1) {
54682
+ const parts = decls[i].split(":");
54683
+ if (parts.length >= 2) {
54684
+ const prop = parts[0].trim().toLowerCase();
54685
+ const val = parts.slice(1).join(":").trim();
54686
+ if (prop === "color") {
54687
+ return val.replace(/['"]+/g, "");
54688
+ }
54689
+ }
54690
+ }
54691
+ }
54692
+ return (_a = element.style.color) == null ? void 0 : _a.replace(/['"]+/g, "");
54693
+ },
54694
+ renderHTML: (attributes) => {
54695
+ if (!attributes.color) {
54696
+ return {};
54697
+ }
54698
+ return {
54699
+ style: `color: ${attributes.color}`
54700
+ };
54701
+ }
54702
+ }
54703
+ }
54704
+ }
54705
+ ];
54706
+ },
54707
+ addCommands() {
54708
+ return {
54709
+ setColor: (color) => ({ chain }) => {
54710
+ return chain().setMark("textStyle", { color }).run();
54711
+ },
54712
+ unsetColor: () => ({ chain }) => {
54713
+ return chain().setMark("textStyle", { color: null }).removeEmptyTextStyle().run();
54714
+ }
54715
+ };
54716
+ }
54717
+ });
54718
+ var FontFamily = Extension.create({
54719
+ name: "fontFamily",
54720
+ addOptions() {
54721
+ return {
54722
+ types: ["textStyle"]
54723
+ };
54724
+ },
54725
+ addGlobalAttributes() {
54726
+ return [
54727
+ {
54728
+ types: this.options.types,
54729
+ attributes: {
54730
+ fontFamily: {
54731
+ default: null,
54732
+ parseHTML: (element) => element.style.fontFamily,
54733
+ renderHTML: (attributes) => {
54734
+ if (!attributes.fontFamily) {
54735
+ return {};
54736
+ }
54737
+ return {
54738
+ style: `font-family: ${attributes.fontFamily}`
54739
+ };
54740
+ }
54741
+ }
54742
+ }
54743
+ }
54744
+ ];
54745
+ },
54746
+ addCommands() {
54747
+ return {
54748
+ setFontFamily: (fontFamily) => ({ chain }) => {
54749
+ return chain().setMark("textStyle", { fontFamily }).run();
54750
+ },
54751
+ unsetFontFamily: () => ({ chain }) => {
54752
+ return chain().setMark("textStyle", { fontFamily: null }).removeEmptyTextStyle().run();
54753
+ }
54754
+ };
54755
+ }
54756
+ });
54757
+ var FontSize = Extension.create({
54758
+ name: "fontSize",
54759
+ addOptions() {
54760
+ return {
54761
+ types: ["textStyle"]
54762
+ };
54763
+ },
54764
+ addGlobalAttributes() {
54765
+ return [
54766
+ {
54767
+ types: this.options.types,
54768
+ attributes: {
54769
+ fontSize: {
54770
+ default: null,
54771
+ parseHTML: (element) => element.style.fontSize,
54772
+ renderHTML: (attributes) => {
54773
+ if (!attributes.fontSize) {
54774
+ return {};
54775
+ }
54776
+ return {
54777
+ style: `font-size: ${attributes.fontSize}`
54778
+ };
54779
+ }
54780
+ }
54781
+ }
54782
+ }
54783
+ ];
54784
+ },
54785
+ addCommands() {
54786
+ return {
54787
+ setFontSize: (fontSize) => ({ chain }) => {
54788
+ return chain().setMark("textStyle", { fontSize }).run();
54789
+ },
54790
+ unsetFontSize: () => ({ chain }) => {
54791
+ return chain().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run();
54792
+ }
54793
+ };
54794
+ }
54795
+ });
54796
+ var LineHeight = Extension.create({
54797
+ name: "lineHeight",
54798
+ addOptions() {
54799
+ return {
54800
+ types: ["textStyle"]
54801
+ };
54802
+ },
54803
+ addGlobalAttributes() {
54804
+ return [
54805
+ {
54806
+ types: this.options.types,
54807
+ attributes: {
54808
+ lineHeight: {
54809
+ default: null,
54810
+ parseHTML: (element) => element.style.lineHeight,
54811
+ renderHTML: (attributes) => {
54812
+ if (!attributes.lineHeight) {
54813
+ return {};
54814
+ }
54815
+ return {
54816
+ style: `line-height: ${attributes.lineHeight}`
54817
+ };
54818
+ }
54819
+ }
54820
+ }
54821
+ }
54822
+ ];
54823
+ },
54824
+ addCommands() {
54825
+ return {
54826
+ setLineHeight: (lineHeight) => ({ chain }) => {
54827
+ return chain().setMark("textStyle", { lineHeight }).run();
54828
+ },
54829
+ unsetLineHeight: () => ({ chain }) => {
54830
+ return chain().setMark("textStyle", { lineHeight: null }).removeEmptyTextStyle().run();
54831
+ }
54832
+ };
54833
+ }
54834
+ });
54835
+ Extension.create({
54836
+ name: "textStyleKit",
54837
+ addExtensions() {
54838
+ const extensions = [];
54839
+ if (this.options.backgroundColor !== false) {
54840
+ extensions.push(BackgroundColor.configure(this.options.backgroundColor));
54841
+ }
54842
+ if (this.options.color !== false) {
54843
+ extensions.push(Color.configure(this.options.color));
54844
+ }
54845
+ if (this.options.fontFamily !== false) {
54846
+ extensions.push(FontFamily.configure(this.options.fontFamily));
54847
+ }
54848
+ if (this.options.fontSize !== false) {
54849
+ extensions.push(FontSize.configure(this.options.fontSize));
54850
+ }
54851
+ if (this.options.lineHeight !== false) {
54852
+ extensions.push(LineHeight.configure(this.options.lineHeight));
54853
+ }
54854
+ if (this.options.textStyle !== false) {
54855
+ extensions.push(TextStyle.configure(this.options.textStyle));
54856
+ }
54857
+ return extensions;
54858
+ }
54859
+ });
54860
+
54861
+ // src/highlight.ts
54862
+ var inputRegex$1 = /(?:^|\s)(==(?!\s+==)((?:[^=]+))==(?!\s+==))$/;
54863
+ var pasteRegex = /(?:^|\s)(==(?!\s+==)((?:[^=]+))==(?!\s+==))/g;
54864
+ var Highlight = Mark.create({
54865
+ name: "highlight",
54866
+ addOptions() {
54867
+ return {
54868
+ multicolor: false,
54869
+ HTMLAttributes: {}
54870
+ };
54871
+ },
54872
+ addAttributes() {
54873
+ if (!this.options.multicolor) {
54874
+ return {};
54875
+ }
54876
+ return {
54877
+ color: {
54878
+ default: null,
54879
+ parseHTML: (element) => element.getAttribute("data-color") || element.style.backgroundColor,
54880
+ renderHTML: (attributes) => {
54881
+ if (!attributes.color) {
54882
+ return {};
54883
+ }
54884
+ return {
54885
+ "data-color": attributes.color,
54886
+ style: `background-color: ${attributes.color}; color: inherit`
54887
+ };
54888
+ }
54889
+ }
54890
+ };
54891
+ },
54892
+ parseHTML() {
54893
+ return [
54894
+ {
54895
+ tag: "mark"
54896
+ }
54897
+ ];
54898
+ },
54899
+ renderHTML({ HTMLAttributes }) {
54900
+ return ["mark", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
54901
+ },
54902
+ renderMarkdown: (node, h) => {
54903
+ return `==${h.renderChildren(node)}==`;
54904
+ },
54905
+ parseMarkdown: (token, h) => {
54906
+ return h.applyMark("highlight", h.parseInline(token.tokens || []));
54907
+ },
54908
+ markdownTokenizer: {
54909
+ name: "highlight",
54910
+ level: "inline",
54911
+ start: (src) => src.indexOf("=="),
54912
+ tokenize(src, _, h) {
54913
+ const rule = /^(==)([^=]+)(==)/;
54914
+ const match = rule.exec(src);
54915
+ if (match) {
54916
+ const innerContent = match[2].trim();
54917
+ const children = h.inlineTokens(innerContent);
54918
+ return {
54919
+ type: "highlight",
54920
+ raw: match[0],
54921
+ text: innerContent,
54922
+ tokens: children
54923
+ };
54924
+ }
54925
+ }
54926
+ },
54927
+ addCommands() {
54928
+ return {
54929
+ setHighlight: (attributes) => ({ commands }) => {
54930
+ return commands.setMark(this.name, attributes);
54931
+ },
54932
+ toggleHighlight: (attributes) => ({ commands }) => {
54933
+ return commands.toggleMark(this.name, attributes);
54934
+ },
54935
+ unsetHighlight: () => ({ commands }) => {
54936
+ return commands.unsetMark(this.name);
54937
+ }
54938
+ };
54939
+ },
54940
+ addKeyboardShortcuts() {
54941
+ return {
54942
+ "Mod-Shift-h": () => this.editor.commands.toggleHighlight()
54943
+ };
54944
+ },
54945
+ addInputRules() {
54946
+ return [
54947
+ markInputRule({
54948
+ find: inputRegex$1,
54949
+ type: this.type
54950
+ })
54951
+ ];
54952
+ },
54953
+ addPasteRules() {
54954
+ return [
54955
+ markPasteRule({
54956
+ find: pasteRegex,
54957
+ type: this.type
54958
+ })
54959
+ ];
54960
+ }
54961
+ });
54962
+
54963
+ // src/text-align.ts
54964
+ var TextAlign = Extension.create({
54965
+ name: "textAlign",
54966
+ addOptions() {
54967
+ return {
54968
+ types: [],
54969
+ alignments: ["left", "center", "right", "justify"],
54970
+ defaultAlignment: null
54971
+ };
54972
+ },
54973
+ addGlobalAttributes() {
54974
+ return [
54975
+ {
54976
+ types: this.options.types,
54977
+ attributes: {
54978
+ textAlign: {
54979
+ default: this.options.defaultAlignment,
54980
+ parseHTML: (element) => {
54981
+ const alignment = element.style.textAlign;
54982
+ return this.options.alignments.includes(alignment) ? alignment : this.options.defaultAlignment;
54983
+ },
54984
+ renderHTML: (attributes) => {
54985
+ if (!attributes.textAlign) {
54986
+ return {};
54987
+ }
54988
+ return { style: `text-align: ${attributes.textAlign}` };
54989
+ }
54990
+ }
54991
+ }
54992
+ }
54993
+ ];
54994
+ },
54995
+ addCommands() {
54996
+ return {
54997
+ setTextAlign: (alignment) => ({ commands }) => {
54998
+ if (!this.options.alignments.includes(alignment)) {
54999
+ return false;
55000
+ }
55001
+ return this.options.types.map((type) => commands.updateAttributes(type, { textAlign: alignment })).some((response) => response);
55002
+ },
55003
+ unsetTextAlign: () => ({ commands }) => {
55004
+ return this.options.types.map((type) => commands.resetAttributes(type, "textAlign")).some((response) => response);
55005
+ },
55006
+ toggleTextAlign: (alignment) => ({ editor, commands }) => {
55007
+ if (!this.options.alignments.includes(alignment)) {
55008
+ return false;
55009
+ }
55010
+ if (editor.isActive({ textAlign: alignment })) {
55011
+ return commands.unsetTextAlign();
55012
+ }
55013
+ return commands.setTextAlign(alignment);
55014
+ }
55015
+ };
55016
+ },
55017
+ addKeyboardShortcuts() {
55018
+ return {
55019
+ "Mod-Shift-l": () => this.editor.commands.setTextAlign("left"),
55020
+ "Mod-Shift-e": () => this.editor.commands.setTextAlign("center"),
55021
+ "Mod-Shift-r": () => this.editor.commands.setTextAlign("right"),
55022
+ "Mod-Shift-j": () => this.editor.commands.setTextAlign("justify")
55023
+ };
55024
+ }
55025
+ });
55026
+
55027
+ // src/image.ts
55028
+ var inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/;
55029
+ var Image = Node3.create({
55030
+ name: "image",
55031
+ addOptions() {
55032
+ return {
55033
+ inline: false,
55034
+ allowBase64: false,
55035
+ HTMLAttributes: {},
55036
+ resize: false
55037
+ };
55038
+ },
55039
+ inline() {
55040
+ return this.options.inline;
55041
+ },
55042
+ group() {
55043
+ return this.options.inline ? "inline" : "block";
55044
+ },
55045
+ draggable: true,
55046
+ addAttributes() {
55047
+ return {
55048
+ src: {
55049
+ default: null
55050
+ },
55051
+ alt: {
55052
+ default: null
55053
+ },
55054
+ title: {
55055
+ default: null
55056
+ },
55057
+ width: {
55058
+ default: null
55059
+ },
55060
+ height: {
55061
+ default: null
55062
+ }
55063
+ };
55064
+ },
55065
+ parseHTML() {
55066
+ return [
55067
+ {
55068
+ tag: this.options.allowBase64 ? "img[src]" : 'img[src]:not([src^="data:"])'
55069
+ }
55070
+ ];
55071
+ },
55072
+ renderHTML({ HTMLAttributes }) {
55073
+ return ["img", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
55074
+ },
55075
+ parseMarkdown: (token, helpers) => {
55076
+ return helpers.createNode("image", {
55077
+ src: token.href,
55078
+ title: token.title,
55079
+ alt: token.text
55080
+ });
55081
+ },
55082
+ renderMarkdown: (node) => {
55083
+ var _a, _b, _c, _d, _e, _f;
55084
+ const src = (_b = (_a = node.attrs) == null ? void 0 : _a.src) != null ? _b : "";
55085
+ const alt = (_d = (_c = node.attrs) == null ? void 0 : _c.alt) != null ? _d : "";
55086
+ const title = (_f = (_e = node.attrs) == null ? void 0 : _e.title) != null ? _f : "";
55087
+ return title ? `![${alt}](${src} "${title}")` : `![${alt}](${src})`;
55088
+ },
55089
+ addNodeView() {
55090
+ if (!this.options.resize || !this.options.resize.enabled || typeof document === "undefined") {
55091
+ return null;
55092
+ }
55093
+ const { directions, minWidth, minHeight, alwaysPreserveAspectRatio } = this.options.resize;
55094
+ return ({ node, getPos, HTMLAttributes, editor }) => {
55095
+ const el = document.createElement("img");
55096
+ Object.entries(HTMLAttributes).forEach(([key, value]) => {
55097
+ if (value != null) {
55098
+ switch (key) {
55099
+ case "width":
55100
+ case "height":
55101
+ break;
55102
+ default:
55103
+ el.setAttribute(key, value);
55104
+ break;
55105
+ }
55106
+ }
55107
+ });
55108
+ el.src = HTMLAttributes.src;
55109
+ const nodeView = new ResizableNodeView({
55110
+ element: el,
55111
+ editor,
55112
+ node,
55113
+ getPos,
55114
+ onResize: (width, height) => {
55115
+ el.style.width = `${width}px`;
55116
+ el.style.height = `${height}px`;
55117
+ },
55118
+ onCommit: (width, height) => {
55119
+ const pos = getPos();
55120
+ if (pos === void 0) {
55121
+ return;
55122
+ }
55123
+ this.editor.chain().setNodeSelection(pos).updateAttributes(this.name, {
55124
+ width,
55125
+ height
55126
+ }).run();
55127
+ },
55128
+ onUpdate: (updatedNode, _decorations, _innerDecorations) => {
55129
+ if (updatedNode.type !== node.type) {
55130
+ return false;
55131
+ }
55132
+ return true;
55133
+ },
55134
+ options: {
55135
+ directions,
55136
+ min: {
55137
+ width: minWidth,
55138
+ height: minHeight
55139
+ },
55140
+ preserveAspectRatio: alwaysPreserveAspectRatio === true
55141
+ }
55142
+ });
55143
+ const dom = nodeView.dom;
55144
+ dom.style.visibility = "hidden";
55145
+ dom.style.pointerEvents = "none";
55146
+ el.onload = () => {
55147
+ dom.style.visibility = "";
55148
+ dom.style.pointerEvents = "";
55149
+ };
55150
+ return nodeView;
55151
+ };
55152
+ },
55153
+ addCommands() {
55154
+ return {
55155
+ setImage: (options) => ({ commands }) => {
55156
+ return commands.insertContent({
55157
+ type: this.name,
55158
+ attrs: options
55159
+ });
55160
+ }
55161
+ };
55162
+ },
55163
+ addInputRules() {
55164
+ return [
55165
+ nodeInputRule({
55166
+ find: inputRegex,
55167
+ type: this.type,
55168
+ getAttributes: (match) => {
55169
+ const [, , alt, src, title] = match;
55170
+ return { src, alt, title };
55171
+ }
55172
+ })
55173
+ ];
55174
+ }
55175
+ });
55176
+
55177
+ class nxEditor extends nxDiv {
55178
+ #editor = null;
55179
+ #container = null;
55180
+
55181
+ constructor() {
55182
+ super();
55183
+ //this.attachShadow({ mode: 'open' });
55184
+ }
55185
+
55186
+ connectedCallback() {
55187
+ if (super.connectedCallback()) {
55188
+ this.#render();
55189
+ this.#initEditor();
55190
+ }
55191
+ }
55192
+
55193
+ #render() {
55194
+ this.shadowRoot.innerHTML = `
55195
+ <style>
55196
+ @import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/nxEditor.css";
55197
+ ${ninegrid.getCustomPath(this,"nxEditor.css")}
55198
+ </style>
55199
+
55200
+ <div class="menu-bar">
55201
+ <button type="button" data-cmd="bold"><b>B</b></button>
55202
+ <button type="button" data-cmd="italic"><i>I</i></button>
55203
+ <button type="button" data-cmd="underline"><u>U</u></button>
55204
+ <button type="button" data-cmd="strike">S</button>
55205
+
55206
+ <input type="color" id="color-picker" title="Text Color">
55207
+ <button type="button" data-cmd="highlight">Highlight</button>
55208
+
55209
+ <button type="button" data-align="left">Left</button>
55210
+ <button type="button" data-align="center">Center</button>
55211
+ <button type="button" data-align="right">Right</button>
55212
+
55213
+ <button type="button" data-cmd="bulletList">Bullet</button>
55214
+ <button type="button" data-cmd="orderedList">Ordered</button>
55215
+
55216
+ <button type="button" id="img-btn">Image</button>
55217
+
55218
+ <button type="button" data-cmd="undo">Undo</button>
55219
+ <button type="button" data-cmd="redo">Redo</button>
55220
+ </div>
55221
+ <div id="editor-container"></div>
55222
+ `;
55223
+ this.#container = this.shadowRoot.querySelector('#editor-container');
55224
+ }
55225
+
55226
+ #initEditor() {
55227
+ this.#editor = new Editor({
55228
+ element: this.#container,
55229
+ extensions: [
55230
+ index_default,
55231
+ Underline,
55232
+ TextStyle,
55233
+ Color,
55234
+ Highlight.configure({ multicolor: true }),
55235
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
55236
+ Image.configure({ inline: true, allowBase64: true }),
55237
+ ],
55238
+ content: this.originContents || '',
55239
+ onUpdate: ({ editor }) => {
55240
+ this.dispatchEvent(new CustomEvent('change', { detail: editor.getHTML() }));
55241
+ }
55242
+ });
55243
+
55244
+ this.#bindEvents();
55245
+ }
55246
+
55247
+ #bindEvents() {
55248
+ const editor = this.#editor;
55249
+
55250
+ // 일반 커맨드 버튼
55251
+ this.shadowRoot.querySelectorAll('[data-cmd]').forEach(btn => {
55252
+ btn.onclick = () => {
55253
+ const cmd = btn.dataset.cmd;
55254
+ if (cmd === 'bold') editor.chain().focus().toggleBold().run();
55255
+ if (cmd === 'italic') editor.chain().focus().toggleItalic().run();
55256
+ if (cmd === 'underline') editor.chain().focus().toggleUnderline().run();
55257
+ if (cmd === 'strike') editor.chain().focus().toggleStrike().run();
55258
+ if (cmd === 'highlight') editor.chain().focus().toggleHighlight().run();
55259
+ if (cmd === 'bulletList') editor.chain().focus().toggleBulletList().run();
55260
+ if (cmd === 'orderedList') editor.chain().focus().toggleOrderedList().run();
55261
+ if (cmd === 'undo') editor.chain().focus().undo().run();
55262
+ if (cmd === 'redo') editor.chain().focus().redo().run();
55263
+ };
55264
+ });
55265
+
55266
+ // 색상 변경
55267
+ this.shadowRoot.querySelector('#color-picker').oninput = (e) => {
55268
+ editor.chain().focus().setColor(e.target.value).run();
55269
+ };
55270
+
55271
+ // 정렬
55272
+ this.shadowRoot.querySelectorAll('[data-align]').forEach(btn => {
55273
+ btn.onclick = () => editor.chain().focus().setTextAlign(btn.dataset.align).run();
55274
+ });
55275
+
55276
+ // 이미지 삽입 (Prompt 방식)
55277
+ this.shadowRoot.querySelector('#img-btn').onclick = () => {
55278
+ const url = window.prompt('Image URL');
55279
+ if (url) editor.chain().focus().setImage({ src: url }).run();
55280
+ };
55281
+ }
55282
+
55283
+ setData = (value) => {
55284
+ if (this.#editor) {
55285
+ this.#editor.commands.setContent(value || "", false);
55286
+ } else {
55287
+ this.originContents = value;
55288
+ }
55289
+ }
55290
+
55291
+ getData = () => {
55292
+ return this.#editor ? this.#editor.getHTML() : "";
55293
+ }
55294
+
55295
+ disconnectedCallback() {
55296
+ if (this.#editor) {
55297
+ this.#editor.destroy();
55298
+ this.#editor = null;
55299
+ }
54060
55300
  }
54061
55301
  }
54062
55302