@zipify/wysiwyg 3.1.2 → 3.1.3-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.
@@ -3,14 +3,17 @@ import { computed, unref } from 'vue';
3
3
  import { createCommand } from '../utils';
4
4
  import { LinkDestinations, LinkTargets, TextSettings } from '../enums';
5
5
  import { NodeFactory } from '../services';
6
+ import { PasteLinkPlugin } from './proseMirror';
6
7
 
7
8
  export const Link = Base.extend({
8
9
  name: TextSettings.LINK,
10
+ addPasteRules: null,
9
11
 
10
12
  addOptions() {
11
13
  return {
12
14
  ...this.parent?.(),
13
- openOnClick: false
15
+ openOnClick: false,
16
+ linkOnPaste: false
14
17
  };
15
18
  },
16
19
 
@@ -52,8 +55,6 @@ export const Link = Base.extend({
52
55
 
53
56
  addCommands() {
54
57
  return {
55
- ...this.parent?.(),
56
-
57
58
  applyLink: createCommand(({ commands, chain }, attributes) => {
58
59
  commands.setMeta('preventAutolink', true);
59
60
 
@@ -67,7 +68,9 @@ export const Link = Base.extend({
67
68
  .applyMark(this.name, attributes)
68
69
  .expandSelectionToLink()
69
70
  .command(({ tr }) => {
70
- tr.insertText(attributes.text, tr.selection.from, tr.selection.to);
71
+ if (attributes.text) {
72
+ tr.insertText(attributes.text, tr.selection.from, tr.selection.to);
73
+ }
71
74
  return true;
72
75
  })
73
76
  .run();
@@ -82,11 +85,17 @@ export const Link = Base.extend({
82
85
  };
83
86
  },
84
87
 
88
+ addProseMirrorPlugins() {
89
+ return [
90
+ ...this.parent(),
91
+ PasteLinkPlugin.create(this.editor)
92
+ ];
93
+ },
94
+
85
95
  renderHTML({ HTMLAttributes: attrs }) {
86
96
  const href = attrs.destination === LinkDestinations.BLOCK ? `#${attrs.href}` : attrs.href;
87
97
  const presetClass = unref(this.options.basePresetClass) + unref(this.options.preset).id;
88
- const classes = `${presetClass} zw-style`;
89
- const linkAttrs = { href, target: attrs.target, class: classes };
98
+ const linkAttrs = { href, target: attrs.target, class: `${presetClass} zw-style` };
90
99
 
91
100
  return ['a', linkAttrs, 0];
92
101
  }
@@ -1,13 +1,13 @@
1
1
  import { Extension } from '@tiptap/vue-2';
2
2
  import Text from '@tiptap/extension-text';
3
3
  import History from '@tiptap/extension-history';
4
+ import { PastePlugin, PlaceholderPlugin } from '../proseMirror';
4
5
  import { NodeProcessor } from './NodeProcessor';
5
6
  import { TextProcessor } from './TextProcessor';
6
7
  import { SelectionProcessor } from './SelectionProcessor';
7
8
  import { Document } from './Document';
8
9
  import { Paragraph } from './Paragraph';
9
10
  import { Heading } from './Heading';
10
- import { PastePlugin, PlaceholderPlugin } from './plugins';
11
11
 
12
12
  const ProseMirrorPlugins = Extension.create({
13
13
  name: 'prose_mirror_plugins',
@@ -0,0 +1,29 @@
1
+ import { test as testLink } from 'linkifyjs';
2
+ import { NodeSelector } from '../../services';
3
+ import { NodeTypes, TextSettings } from '../../enums';
4
+ import { ProseMirrorPlugin } from './ProseMirrorPlugin';
5
+
6
+ export class PasteLinkPlugin extends ProseMirrorPlugin {
7
+ addProps() {
8
+ return { handlePaste: this._handlePaste };
9
+ }
10
+
11
+ _handlePaste(view, event, slice) {
12
+ if (view.state.selection.empty) return false;
13
+
14
+ const textContent = slice.content.textBetween(0, slice.content.size).trim();
15
+ const isLink = testLink(textContent);
16
+
17
+ if (!textContent || !isLink) return false;
18
+
19
+ const pastingLink = NodeSelector.query(slice.content, {
20
+ typeName: NodeTypes.TEXT,
21
+ mark: { typeName: TextSettings.LINK },
22
+ getMark: { typeName: TextSettings.LINK }
23
+ });
24
+
25
+ this.editor.commands.applyLink(pastingLink.attrs);
26
+
27
+ return true;
28
+ }
29
+ }
@@ -1,12 +1,12 @@
1
- import { ContentNormalizer } from '../../../services';
2
- import { NodeTypes } from '../../../enums';
1
+ import { ContentNormalizer } from '../../services';
2
+ import { NodeTypes } from '../../enums';
3
3
  import { ProseMirrorPlugin } from './ProseMirrorPlugin';
4
4
 
5
5
  export class PastePlugin extends ProseMirrorPlugin {
6
- buildProps() {
6
+ addProps() {
7
7
  return {
8
- transformPastedHTML: this._transformPastedHTML.bind(this),
9
- handlePaste: this._handlePaste.bind(this)
8
+ transformPastedHTML: this._transformPastedHTML,
9
+ handlePaste: this._handlePaste
10
10
  };
11
11
  }
12
12
 
@@ -2,8 +2,8 @@ import { Decoration, DecorationSet } from 'prosemirror-view';
2
2
  import { ProseMirrorPlugin } from './ProseMirrorPlugin';
3
3
 
4
4
  export class PlaceholderPlugin extends ProseMirrorPlugin {
5
- buildProps() {
6
- return { decorations: this._buildDecorations.bind(this) };
5
+ addProps() {
6
+ return { decorations: this._buildDecorations };
7
7
  }
8
8
 
9
9
  _buildDecorations({ doc }) {
@@ -6,7 +6,7 @@ export class ProseMirrorPlugin {
6
6
 
7
7
  return new Plugin({
8
8
  key: new PluginKey(this.name),
9
- props: plugin.buildProps()
9
+ props: plugin._buildProps()
10
10
  });
11
11
  }
12
12
 
@@ -15,7 +15,14 @@ export class ProseMirrorPlugin {
15
15
  this.editor = editor;
16
16
  }
17
17
 
18
- buildProps() {
18
+ _buildProps() {
19
+ const props = Object.entries(this.addProps());
20
+ const bound = props.map(([name, handler]) => [name, handler.bind(this)]);
21
+
22
+ return Object.fromEntries(bound);
23
+ }
24
+
25
+ addProps() {
19
26
  return {};
20
27
  }
21
28
  }
@@ -1,2 +1,3 @@
1
1
  export { PastePlugin } from './PastePlugin';
2
2
  export { PlaceholderPlugin } from './PlaceholderPlugin';
3
+ export { PasteLinkPlugin } from './PasteLinkPlugin';
@@ -111,7 +111,7 @@ export class NodeFactory {
111
111
 
112
112
  /**
113
113
  * @param {String} text
114
- * @param {Array<MarkJson>} marks
114
+ * @param {Array<MarkJson>} [marks]
115
115
  * @returns {NodeJson}
116
116
  */
117
117
  static text(text, marks) {
@@ -0,0 +1,50 @@
1
+ export class NodeSelector {
2
+ static _instance;
3
+
4
+ static get instance() {
5
+ this._instance ??= new NodeSelector();
6
+ return this._instance;
7
+ }
8
+
9
+ static query(containerNode, selector) {
10
+ return this.instance.query(containerNode, selector);
11
+ }
12
+
13
+ query(containerNode, selector) {
14
+ let found = null;
15
+
16
+ containerNode.descendants((node) => {
17
+ if (found) return false;
18
+
19
+ if (this.matchNode(node, selector)) {
20
+ found = node;
21
+ return false;
22
+ }
23
+ });
24
+
25
+ if (!found) return null;
26
+ if (!selector.getMark) return found;
27
+
28
+ return this.getMark(found, selector.getMark);
29
+ }
30
+
31
+ matchNode(node, selector) {
32
+ if (selector.typeName && selector.typeName !== node.type.name) {
33
+ return false;
34
+ }
35
+
36
+ if (selector.mark && !this.getMark(node, selector.mark)) {
37
+ return false;
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ getMark(node, selector) {
44
+ return node.marks.find((mark) => this.matchMark(mark, selector)) || null;
45
+ }
46
+
47
+ matchMark(mark, selector) {
48
+ return mark.type.name === selector.typeName;
49
+ }
50
+ }
@@ -0,0 +1,129 @@
1
+ import { Schema } from 'prosemirror-model';
2
+ import { NodeFactory } from '../NodeFactory';
3
+ import { NodeSelector } from '../NodeSelector';
4
+ import { NodeTypes, TextSettings } from '../../enums';
5
+
6
+ const createSchema = () => new Schema({
7
+ nodes: {
8
+ [NodeTypes.DOCUMENT]: { content: 'paragraph+' },
9
+ [NodeTypes.PARAGRAPH]: { content: 'text*' },
10
+ [NodeTypes.TEXT]: { inline: true }
11
+ },
12
+ marks: {
13
+ [TextSettings.FONT_SIZE]: {}
14
+ }
15
+ });
16
+
17
+ const createSelector = () => new NodeSelector();
18
+
19
+ describe('match node', () => {
20
+ test('should match node by type name', () => {
21
+ const schema = createSchema();
22
+ const selector = createSelector();
23
+ const node = schema.nodeFromJSON(NodeFactory.text('lorem ipsum'));
24
+ const isMatch = selector.matchNode(node, { typeName: NodeTypes.TEXT });
25
+
26
+ expect(isMatch).toBe(true);
27
+ });
28
+
29
+ test('should not match node by type name', () => {
30
+ const schema = createSchema();
31
+ const selector = createSelector();
32
+ const node = schema.nodeFromJSON(NodeFactory.text('lorem ipsum'));
33
+ const isMatch = selector.matchNode(node, { typeName: NodeTypes.PARAGRAPH });
34
+
35
+ expect(isMatch).toBe(false);
36
+ });
37
+
38
+ test('should match node with mark', () => {
39
+ const schema = createSchema();
40
+ const selector = createSelector();
41
+
42
+ const node = schema.nodeFromJSON(NodeFactory.text('lorem ipsum', [
43
+ NodeFactory.mark(TextSettings.FONT_SIZE, {})
44
+ ]));
45
+
46
+ const isMatch = selector.matchNode(node, {
47
+ mark: { typeName: TextSettings.FONT_SIZE }
48
+ });
49
+
50
+ expect(isMatch).toBe(true);
51
+ });
52
+
53
+ test('should not match node with another mark', () => {
54
+ const schema = createSchema();
55
+ const selector = createSelector();
56
+
57
+ const node = schema.nodeFromJSON(NodeFactory.text('lorem ipsum', [
58
+ NodeFactory.mark(TextSettings.FONT_SIZE, {})
59
+ ]));
60
+
61
+ const isMatch = selector.matchNode(node, {
62
+ mark: { typeName: TextSettings.FONT_FAMILY }
63
+ });
64
+
65
+ expect(isMatch).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe('match mark', () => {
70
+ test('should match mark by type name', () => {
71
+ const schema = createSchema();
72
+ const selector = createSelector();
73
+ const mark = schema.markFromJSON(NodeFactory.mark(TextSettings.FONT_SIZE, {}));
74
+ const isMatch = selector.matchMark(mark, { typeName: TextSettings.FONT_SIZE });
75
+
76
+ expect(isMatch).toBe(true);
77
+ });
78
+
79
+ test('should not match mark by type name', () => {
80
+ const schema = createSchema();
81
+ const selector = createSelector();
82
+ const mark = schema.markFromJSON(NodeFactory.mark(TextSettings.FONT_SIZE, {}));
83
+ const isMatch = selector.matchMark(mark, { typeName: TextSettings.FONT_FAMILY });
84
+
85
+ expect(isMatch).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('query node', () => {
90
+ test('should find node', () => {
91
+ const schema = createSchema();
92
+ const selector = createSelector();
93
+
94
+ const doc = schema.nodeFromJSON(NodeFactory.doc([
95
+ NodeFactory.paragraph('lorem ipsum')
96
+ ]));
97
+
98
+ const textNode = selector.query(doc, { typeName: NodeTypes.TEXT });
99
+
100
+ expect(textNode.text).toBe('lorem ipsum');
101
+ });
102
+
103
+ test('should find first matching node', () => {
104
+ const schema = createSchema();
105
+ const selector = createSelector();
106
+
107
+ const doc = schema.nodeFromJSON(NodeFactory.doc([
108
+ NodeFactory.paragraph('lorem ipsum 1'),
109
+ NodeFactory.paragraph('lorem ipsum 2')
110
+ ]));
111
+
112
+ const textNode = selector.query(doc, { typeName: NodeTypes.TEXT });
113
+
114
+ expect(textNode.text).toBe('lorem ipsum 1');
115
+ });
116
+
117
+ test('should not find any node', () => {
118
+ const schema = createSchema();
119
+ const selector = createSelector();
120
+
121
+ const doc = schema.nodeFromJSON(NodeFactory.doc([
122
+ NodeFactory.paragraph('lorem ipsum 1')
123
+ ]));
124
+
125
+ const node = selector.query(doc, { typeName: NodeTypes.LIST });
126
+
127
+ expect(node).toBeNull();
128
+ });
129
+ });
@@ -7,3 +7,4 @@ export { ContextWindow } from './ContextWindow';
7
7
  export { NodeFactory } from './NodeFactory';
8
8
  export { HtmlToJsonParser } from './HtmlToJsonParser';
9
9
  export { StylePresetRenderer } from './StylePresetRenderer';
10
+ export { NodeSelector } from './NodeSelector';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zipify/wysiwyg",
3
- "version": "3.1.2",
3
+ "version": "3.1.3-0",
4
4
  "description": "Zipify modification of TipTap text editor",
5
5
  "main": "dist/wysiwyg.mjs",
6
6
  "bin": {