vuewrite 0.0.8 → 0.0.10

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.
@@ -9,6 +9,8 @@ import { Ref } from 'vue';
9
9
 
10
10
  declare type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
11
11
 
12
+ declare type __VLS_NonUndefinedable_2<T> = T extends undefined ? never : T;
13
+
12
14
  declare type __VLS_TypePropsToRuntimeProps<T> = {
13
15
  [K in keyof T]-?: {} extends Pick<T, K> ? {
14
16
  type: PropType<__VLS_NonUndefinedable<T[K]>>;
@@ -18,6 +20,15 @@ declare type __VLS_TypePropsToRuntimeProps<T> = {
18
20
  };
19
21
  };
20
22
 
23
+ declare type __VLS_TypePropsToRuntimeProps_2<T> = {
24
+ [K in keyof T]-?: {} extends Pick<T, K> ? {
25
+ type: PropType<__VLS_NonUndefinedable_2<T[K]>>;
26
+ } : {
27
+ type: PropType<T[K]>;
28
+ required: true;
29
+ };
30
+ };
31
+
21
32
  declare type __VLS_WithTemplateSlots<T, S> = T & {
22
33
  new (): {
23
34
  $slots: S;
@@ -34,6 +45,12 @@ export declare type Block = {
34
45
 
35
46
  export declare type Decorator = (style: Style) => HTMLAttributes | undefined;
36
47
 
48
+ declare type HistoryAction = {
49
+ type: "insertText" | "setText";
50
+ blocks: Block[];
51
+ selection: TextEditorSelection;
52
+ };
53
+
37
54
  export declare type Style = {
38
55
  start: number;
39
56
  end: number;
@@ -49,9 +66,11 @@ text: string;
49
66
  styles?: Style[] | undefined;
50
67
  type?: string | undefined;
51
68
  }[] | undefined;
69
+ parser?: TextParser | undefined;
52
70
  styles?: Style[] | undefined;
53
71
  autofocus?: boolean | undefined;
54
72
  autoselect?: boolean | undefined;
73
+ preventMultiline?: boolean | undefined;
55
74
  }>, {
56
75
  currentStyles: ComputedRef<Map<string, Style>>;
57
76
  currentBlock: ComputedRef< {
@@ -86,6 +105,7 @@ insertBlock: (blockData: Partial<Block>) => void;
86
105
  addNewLine: () => void;
87
106
  removeNewLine: () => void;
88
107
  selectAll: () => void;
108
+ pushHistory: (type: "insertText" | "setText") => void;
89
109
  getClientRects: (selection: TextEditorSelection) => DOMRectList;
90
110
  }, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
91
111
  keydown: (...args: any[]) => void;
@@ -99,9 +119,11 @@ text: string;
99
119
  styles?: Style[] | undefined;
100
120
  type?: string | undefined;
101
121
  }[] | undefined;
122
+ parser?: TextParser | undefined;
102
123
  styles?: Style[] | undefined;
103
124
  autofocus?: boolean | undefined;
104
125
  autoselect?: boolean | undefined;
126
+ preventMultiline?: boolean | undefined;
105
127
  }>>> & {
106
128
  onKeydown?: ((...args: any[]) => any) | undefined;
107
129
  "onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
@@ -110,9 +132,22 @@ onKeydown?: ((...args: any[]) => any) | undefined;
110
132
  placeholder?(_: {}): any;
111
133
  }>;
112
134
 
135
+ declare class TextEditorHistory {
136
+ actions: HistoryAction[];
137
+ currentCursor: number;
138
+ private store;
139
+ constructor(store: TextEditorStore);
140
+ tickStarted: boolean;
141
+ push(type: HistoryAction["type"]): void;
142
+ private applyAction;
143
+ undo(): void;
144
+ redo(): void;
145
+ }
146
+
113
147
  export declare type TextEditorRef = Pick<TextEditorStore, "currentStyles" | "currentBlock" | "isCollapsed" | "selection" | "toggleStyle" | "applyStyle" | "removeStyle" | "insertText" | "insertBlock" | "addNewLine" | "removeNewLine" | "selectAll"> & {
114
148
  isFocused: boolean;
115
149
  getClientRects: (selection: TextEditorSelection) => DOMRectList;
150
+ pushHistory: TextEditorHistory["push"];
116
151
  };
117
152
 
118
153
  declare type TextEditorSelection = {
@@ -127,6 +162,7 @@ declare type TextEditorSelection = {
127
162
  };
128
163
 
129
164
  declare class TextEditorStore {
165
+ history: TextEditorHistory;
130
166
  blocks: {
131
167
  id: string;
132
168
  text: string;
@@ -204,4 +240,26 @@ declare class TextEditorStore {
204
240
  selectAll(): void;
205
241
  }
206
242
 
243
+ export declare const TextEditorView: DefineComponent<__VLS_TypePropsToRuntimeProps_2<{
244
+ modelValue: {
245
+ text: string;
246
+ styles?: Style[];
247
+ type?: string;
248
+ }[] | string;
249
+ decorator?: Decorator | undefined;
250
+ parser?: TextParser | undefined;
251
+ styles?: Style[] | undefined;
252
+ }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps_2<{
253
+ modelValue: {
254
+ text: string;
255
+ styles?: Style[];
256
+ type?: string;
257
+ }[] | string;
258
+ decorator?: Decorator | undefined;
259
+ parser?: TextParser | undefined;
260
+ styles?: Style[] | undefined;
261
+ }>>>, {}, {}>;
262
+
263
+ declare type TextParser = (text: string) => Style[];
264
+
207
265
  export { }
package/dist/vuewrite.js CHANGED
@@ -4,7 +4,7 @@ var __publicField = (obj, key, value) => {
4
4
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
  return value;
6
6
  };
7
- import { getCurrentScope, onScopeDispose, unref, watch, reactive, computed, ref, defineComponent, getCurrentInstance, openBlock, createBlock, resolveDynamicComponent, createElementBlock, normalizeProps, mergeProps, h, nextTick, useSlots, onMounted, Fragment, renderList, renderSlot, createCommentVNode } from "vue";
7
+ import { getCurrentScope, onScopeDispose, unref, watch, nextTick, reactive, computed, ref, defineComponent, getCurrentInstance, openBlock, createBlock, resolveDynamicComponent, createElementBlock, normalizeProps, mergeProps, h, useSlots, onMounted, Fragment, renderList, renderSlot, createCommentVNode } from "vue";
8
8
  function tryOnScopeDispose(fn) {
9
9
  if (getCurrentScope()) {
10
10
  onScopeDispose(fn);
@@ -140,8 +140,85 @@ const isEqual = (a, b) => {
140
140
  }
141
141
  return true;
142
142
  };
143
+ class TextEditorHistory {
144
+ constructor(store) {
145
+ __publicField(this, "actions", []);
146
+ __publicField(this, "currentCursor", 0);
147
+ __publicField(this, "store");
148
+ __publicField(this, "tickStarted", false);
149
+ this.store = store;
150
+ window.showHistory = () => console.log(this.actions);
151
+ }
152
+ push(type) {
153
+ var _a;
154
+ if (this.currentCursor + 1 < this.actions.length) {
155
+ this.actions.length = this.currentCursor + 1;
156
+ }
157
+ const lastAction = this.actions.length > 0 && this.currentCursor === this.actions.length - 1 ? this.actions[this.actions.length - 1] : null;
158
+ const selection = JSON.parse(JSON.stringify(this.store.selection));
159
+ if (type === "insertText") {
160
+ const currentBlock = JSON.parse(JSON.stringify(this.store.currentBlock));
161
+ if (lastAction && lastAction.type === type && ((_a = lastAction.blocks[0]) == null ? void 0 : _a.id) === currentBlock.id) {
162
+ lastAction.blocks = [currentBlock];
163
+ lastAction.selection = selection;
164
+ } else {
165
+ this.actions.push({ type, blocks: [currentBlock], selection });
166
+ }
167
+ } else if (type === "setText") {
168
+ const blocks = JSON.parse(JSON.stringify(this.store.blocks));
169
+ if (this.tickStarted && lastAction) {
170
+ lastAction.blocks = blocks;
171
+ lastAction.selection = selection;
172
+ } else {
173
+ this.actions.push({ type: "setText", blocks, selection });
174
+ }
175
+ }
176
+ this.currentCursor = this.actions.length - 1;
177
+ if (!this.tickStarted && type === "setText") {
178
+ this.tickStarted = true;
179
+ nextTick(() => {
180
+ this.tickStarted = false;
181
+ });
182
+ }
183
+ }
184
+ applyAction(action) {
185
+ if (action.type === "setText") {
186
+ for (let i = 0; i < action.blocks.length; i++) {
187
+ if (i >= this.store.blocks.length) {
188
+ this.store.blocks.push({ ...action.blocks[i] });
189
+ } else {
190
+ Object.assign(this.store.blocks[i], action.blocks[i]);
191
+ }
192
+ }
193
+ if (this.store.blocks.length > action.blocks.length) {
194
+ this.store.blocks.length = action.blocks.length;
195
+ }
196
+ } else {
197
+ for (let _block of action.blocks) {
198
+ const block = this.store.blocks.find((block2) => block2.id === _block.id);
199
+ if (block) {
200
+ block.text = _block.text;
201
+ }
202
+ }
203
+ }
204
+ Object.assign(this.store.selection, action.selection);
205
+ }
206
+ undo() {
207
+ if (this.currentCursor === 0)
208
+ return;
209
+ this.currentCursor--;
210
+ this.applyAction(this.actions[this.currentCursor]);
211
+ }
212
+ redo() {
213
+ if (this.currentCursor >= this.actions.length - 1)
214
+ return;
215
+ this.currentCursor++;
216
+ this.applyAction(this.actions[this.currentCursor]);
217
+ }
218
+ }
143
219
  class TextEditorStore {
144
220
  constructor() {
221
+ __publicField(this, "history", new TextEditorHistory(this));
145
222
  __publicField(this, "blocks", reactive([{ id: uid(), text: "", styles: [] }]));
146
223
  __publicField(this, "selection", reactive({
147
224
  anchor: { blockId: this.blocks[0].id, offset: 0 },
@@ -214,16 +291,17 @@ class TextEditorStore {
214
291
  const blockIndex = this.blocks.findIndex((item) => item.id === this.selection.anchor.blockId);
215
292
  if (blockIndex < 1)
216
293
  return;
294
+ this.selection.anchor.blockId = this.blocks[blockIndex - 1].id;
295
+ this.selection.focus.blockId = this.blocks[blockIndex - 1].id;
296
+ this.selection.anchor.offset = this.blocks[blockIndex - 1].text.length;
297
+ this.selection.focus.offset = this.blocks[blockIndex - 1].text.length;
217
298
  if (this.blocks[blockIndex - 1].editable === false) {
218
299
  this.blocks.splice(blockIndex - 1, 1);
219
300
  } else {
220
301
  this.concatBlocks(this.blocks[blockIndex - 1], this.blocks[blockIndex]);
221
302
  this.blocks.splice(blockIndex, 1);
222
303
  }
223
- this.selection.anchor.blockId = this.blocks[blockIndex - 1].id;
224
- this.selection.focus.blockId = this.blocks[blockIndex - 1].id;
225
- this.selection.anchor.offset = this.blocks[blockIndex - 1].text.length;
226
- this.selection.focus.offset = this.blocks[blockIndex - 1].text.length;
304
+ this.history.push("setText");
227
305
  }
228
306
  onInput(_e) {
229
307
  const ev = _e;
@@ -266,6 +344,7 @@ class TextEditorStore {
266
344
  }
267
345
  if (ev.inputType === "insertText") {
268
346
  this.insertText(ev.data);
347
+ this.history.push("insertText");
269
348
  }
270
349
  }
271
350
  addNewLine() {
@@ -279,13 +358,15 @@ class TextEditorStore {
279
358
  this.blocks.splice(index + 1, 0, block);
280
359
  this.selection.anchor = { blockId: block.id, offset: 0 };
281
360
  this.selection.focus = { blockId: block.id, offset: 0 };
361
+ this.history.push("setText");
282
362
  }
283
363
  insertText(data) {
284
364
  const block = this.currentBlock;
285
365
  if (!block)
286
366
  return;
287
- block.text = block.text.slice(0, this.selection.focus.offset) + data + block.text.slice(this.selection.focus.offset);
288
- this.moveOffset(clamp(this.selection.focus.offset + data.length, 0, block.text.length));
367
+ const text = data.replace(/\r/g, "");
368
+ block.text = block.text.slice(0, this.selection.focus.offset) + text + block.text.slice(this.selection.focus.offset);
369
+ this.moveOffset(clamp(this.selection.focus.offset + text.length, 0, block.text.length));
289
370
  }
290
371
  insertBlock(blockData) {
291
372
  if (!this.currentBlock)
@@ -303,6 +384,7 @@ class TextEditorStore {
303
384
  if (blockData.editable === false && this.currentBlock === this.blocks[this.blocks.length - 1]) {
304
385
  this.addNewLine();
305
386
  }
387
+ this.history.push("setText");
306
388
  }
307
389
  get startAndEnd() {
308
390
  const a = this.blocks.findIndex((block) => block.id === this.selection.anchor.blockId);
@@ -442,12 +524,13 @@ class TextEditorStore {
442
524
  }
443
525
  let uidCounter = 0;
444
526
  const uid = () => (uidCounter++).toString();
445
- const _sfc_main$1 = /* @__PURE__ */ defineComponent({
527
+ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
446
528
  __name: "TextEditorBlock",
447
529
  props: {
448
530
  block: {},
449
531
  slots: {},
450
- decorator: { type: Function }
532
+ decorator: { type: Function },
533
+ parser: {}
451
534
  },
452
535
  emits: ["postrender"],
453
536
  setup(__props, { emit: __emit }) {
@@ -542,9 +625,17 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
542
625
  text = text + "\n";
543
626
  }
544
627
  const markers = [];
545
- for (let style of block.styles) {
546
- markers.push([style.start, style]);
547
- markers.push([style.end, style]);
628
+ if (block.styles) {
629
+ for (let style of block.styles) {
630
+ markers.push([style.start, style]);
631
+ markers.push([style.end, style]);
632
+ }
633
+ }
634
+ if (props.parser) {
635
+ for (let style of props.parser(text)) {
636
+ markers.push([style.start, style]);
637
+ markers.push([style.end, style]);
638
+ }
548
639
  }
549
640
  markers.sort((a, b) => a[0] - b[0]);
550
641
  let currentIndex = 0;
@@ -582,15 +673,17 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
582
673
  };
583
674
  }
584
675
  });
585
- const _sfc_main = /* @__PURE__ */ defineComponent({
676
+ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
586
677
  __name: "TextEditor",
587
678
  props: {
588
679
  decorator: { type: Function },
589
680
  single: { type: Boolean },
590
681
  modelValue: {},
682
+ parser: { type: Function },
591
683
  styles: {},
592
684
  autofocus: { type: Boolean },
593
- autoselect: { type: Boolean }
685
+ autoselect: { type: Boolean },
686
+ preventMultiline: { type: Boolean }
594
687
  },
595
688
  emits: ["keydown", "update:modelValue", "update:styles"],
596
689
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -649,7 +742,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
649
742
  return;
650
743
  if (e.code === "Enter") {
651
744
  e.preventDefault();
652
- if (e.shiftKey || props.single) {
745
+ if (!props.preventMultiline && (e.shiftKey || props.single)) {
653
746
  store.insertText("\n");
654
747
  } else {
655
748
  store.addNewLine();
@@ -660,24 +753,34 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
660
753
  e.preventDefault();
661
754
  }
662
755
  }
756
+ if ((e.ctrlKey || e.metaKey) && e.code === "KeyZ" && !e.shiftKey) {
757
+ e.preventDefault();
758
+ store.history.undo();
759
+ }
760
+ if ((e.ctrlKey || e.metaKey) && (e.code === "KeyY" || e.shiftKey && e.code === "KeyZ")) {
761
+ e.preventDefault();
762
+ store.history.redo();
763
+ }
663
764
  };
664
765
  let cachedSelection = {};
665
766
  useEventListener(document, "selectionchange", () => {
666
767
  const sel = window.getSelection();
667
768
  const anchor = findParent(sel.anchorNode, (el) => el.hasAttribute("data-vw-block-id") && el.parentElement === textEditorRef.value);
668
769
  if (anchor) {
669
- const offset = anchor === sel.anchorNode ? 0 : calcOffsetToNode(anchor, sel.anchorNode) + sel.anchorOffset;
770
+ const isNonEditable = anchor.getAttribute("contenteditable") === "false";
771
+ const offset = isNonEditable || anchor === sel.focusNode ? 0 : calcOffsetToNode(anchor, sel.anchorNode) + sel.anchorOffset;
670
772
  store.selection.anchor = { blockId: anchor.getAttribute("data-vw-block-id"), offset };
671
773
  }
672
774
  const focus = findParent(sel.focusNode, (el) => el.hasAttribute("data-vw-block-id") && el.parentElement === textEditorRef.value);
673
775
  if (focus) {
674
- const offset = anchor === sel.focusNode ? 0 : calcOffsetToNode(focus, sel.focusNode) + sel.focusOffset;
776
+ const isNonEditable = focus.getAttribute("contenteditable") === "false";
777
+ const offset = isNonEditable || focus === sel.focusNode ? 0 : calcOffsetToNode(focus, sel.focusNode) + sel.focusOffset;
675
778
  store.selection.focus = { blockId: focus.getAttribute("data-vw-block-id"), offset };
676
779
  }
677
780
  if (store.isFocused.value !== (!!focus || !!anchor)) {
678
781
  store.isFocused.value = !!focus || !!anchor;
679
782
  }
680
- if (!anchor && !focus) {
783
+ if (anchor && focus) {
681
784
  cachedSelection = JSON.parse(JSON.stringify(store.selection));
682
785
  }
683
786
  });
@@ -732,15 +835,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
732
835
  (_b = textEditorRef.value) == null ? void 0 : _b.focus();
733
836
  store.selectAll();
734
837
  }
838
+ store.history.push("setText");
735
839
  });
736
840
  const onCopy = (e) => {
737
841
  e.preventDefault();
738
842
  navigator.clipboard.writeText(store.selectedText);
843
+ store.history.push("setText");
739
844
  };
740
845
  const onCut = (e) => {
741
846
  e.preventDefault();
742
847
  navigator.clipboard.writeText(store.selectedText);
743
848
  store.deleteSelected();
849
+ store.history.push("setText");
744
850
  };
745
851
  const onPaste = (e) => {
746
852
  var _a;
@@ -749,6 +855,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
749
855
  if (!text)
750
856
  return;
751
857
  store.insertText(text);
858
+ store.history.push("setText");
752
859
  };
753
860
  const getClientRects = (selection) => {
754
861
  const anchor = getNode(selection.anchor.blockId);
@@ -775,6 +882,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
775
882
  addNewLine: store.addNewLine.bind(store),
776
883
  removeNewLine: store.removeNewLine.bind(store),
777
884
  selectAll: store.selectAll.bind(store),
885
+ pushHistory: store.history.push.bind(store.history),
778
886
  getClientRects
779
887
  });
780
888
  return (_ctx, _cache) => {
@@ -790,19 +898,53 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
790
898
  onCut
791
899
  }, [
792
900
  (openBlock(true), createElementBlock(Fragment, null, renderList(unref(store).blocks, (block) => {
793
- return openBlock(), createBlock(_sfc_main$1, {
901
+ return openBlock(), createBlock(_sfc_main$2, {
794
902
  key: block.id,
795
903
  block,
796
904
  slots: unref(slots),
797
905
  decorator: props.decorator,
906
+ parser: props.parser,
798
907
  onPostrender: onPostRender
799
- }, null, 8, ["block", "slots", "decorator"]);
908
+ }, null, 8, ["block", "slots", "decorator", "parser"]);
800
909
  }), 128)),
801
910
  unref(store).blocks.length === 1 && unref(store).blocks[0].text === "" && !unref(store).blocks[0].type ? renderSlot(_ctx.$slots, "placeholder", { key: 0 }) : createCommentVNode("", true)
802
911
  ], 544);
803
912
  };
804
913
  }
805
914
  });
915
+ const _sfc_main = /* @__PURE__ */ defineComponent({
916
+ __name: "TextEditorView",
917
+ props: {
918
+ modelValue: {},
919
+ decorator: { type: Function },
920
+ parser: {},
921
+ styles: {}
922
+ },
923
+ setup(__props) {
924
+ const props = __props;
925
+ const slots = useSlots();
926
+ const blocks = computed(() => {
927
+ if (Array.isArray(props.modelValue)) {
928
+ return props.modelValue;
929
+ }
930
+ return [{ text: props.modelValue, styles: props.styles ?? [] }];
931
+ });
932
+ return (_ctx, _cache) => {
933
+ return openBlock(), createElementBlock("div", null, [
934
+ (openBlock(true), createElementBlock(Fragment, null, renderList(blocks.value, (block) => {
935
+ return openBlock(), createBlock(_sfc_main$2, {
936
+ key: block.id,
937
+ block,
938
+ slots: unref(slots),
939
+ decorator: props.decorator,
940
+ parser: props.parser
941
+ }, null, 8, ["block", "slots", "decorator", "parser"]);
942
+ }), 128))
943
+ ]);
944
+ };
945
+ }
946
+ });
806
947
  export {
807
- _sfc_main as TextEditor
948
+ _sfc_main$1 as TextEditor,
949
+ _sfc_main as TextEditorView
808
950
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "vuewrite",
3
3
  "description": "Rich Text Editor based on Vue3 reactivity",
4
4
  "private": false,
5
- "version": "0.0.8",
5
+ "version": "0.0.10",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "author": "den59k",