@zipify/wysiwyg 2.0.0-2 → 2.0.0-4

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.
@@ -12,6 +12,10 @@
12
12
  </option>
13
13
  </select>
14
14
 
15
+ <button type="button" class="zw-load-content" @click="saveContent">
16
+ Save Content
17
+ </button>
18
+
15
19
  <button type="button" class="zw-load-content" @click="loadContent">
16
20
  Load Content
17
21
  </button>
@@ -45,7 +49,7 @@
45
49
  </template>
46
50
 
47
51
  <script>
48
- import { computed, onMounted, ref } from 'vue';
52
+ import { computed, onMounted, ref, unref } from 'vue';
49
53
  import { Wysiwyg } from '../lib/entry-lib';
50
54
  import { FONTS } from './fonts';
51
55
  import { PRESETS, renderPresetVariable } from './presets';
@@ -109,6 +113,10 @@ export default {
109
113
  window.location.reload();
110
114
  }
111
115
 
116
+ function saveContent() {
117
+ sessionStorage.setItem('wswg-data', JSON.stringify(unref(content)));
118
+ }
119
+
112
120
  document.addEventListener('click', (event) => {
113
121
  isActive.value = wswgRef.value.$el.contains(event.target);
114
122
  });
@@ -121,6 +129,7 @@ export default {
121
129
  favoriteColors,
122
130
  updateFavoriteColors,
123
131
  loadContent,
132
+ saveContent,
124
133
  device,
125
134
  updatedAt,
126
135
  presets,
package/lib/entry-cli.js CHANGED
@@ -9,6 +9,7 @@ function rubifyJSON(object) {
9
9
  const json = JSON.stringify(object, skipNullValue, 2);
10
10
 
11
11
  return json
12
+ .replace(/'/g, '\'')
12
13
  .replace(/^[\t ]*"[^:\n\r]+(?<!\\)":/gm, (match) => match.replace(/"/g, ''))
13
14
  .replace(/: "(.+)"([,\n])/g, ': \'$1\'$2');
14
15
  }
@@ -19,7 +20,7 @@ program.command('to-json')
19
20
  .action((html, { config }) => {
20
21
  const configPath = resolve(process.cwd(), config);
21
22
  const serializer = ContentSerializer.build(require(configPath).editor);
22
- const json = rubifyJSON(serializer.toJSON(html));
23
+ const json = rubifyJSON(serializer.toJSON(html.replace(/\\"/g, '"')));
23
24
 
24
25
  // eslint-disable-next-line no-console
25
26
  console.log(json);
@@ -18,7 +18,7 @@ export const TextSettings = Object.freeze({
18
18
  },
19
19
 
20
20
  get inlineMarks() {
21
- return [this.TEXT_DECORATION, this.LINK, this.SUPERSCRIPT];
21
+ return [this.TEXT_DECORATION, this.LINK, this.SUPERSCRIPT, this.BACKGROUND_COLOR];
22
22
  },
23
23
 
24
24
  get marks() {
@@ -1,10 +1,9 @@
1
1
  import { Mark } from '@tiptap/vue-2';
2
2
  import { convertColor, createCommand, renderMark } from '../utils';
3
- import { MarkGroups, TextSettings } from '../enums';
3
+ import { TextSettings } from '../enums';
4
4
 
5
5
  export const BackgroundColor = Mark.create({
6
6
  name: TextSettings.BACKGROUND_COLOR,
7
- group: MarkGroups.SETTINGS,
8
7
 
9
8
  addAttributes: () => ({
10
9
  value: { required: true }
@@ -176,7 +176,7 @@ export const StylePreset = Extension.create({
176
176
  chain()
177
177
  .storeSelection()
178
178
  .expandSelectionToBlock()
179
- .unsetAllMarks()
179
+ .removeAllMarks()
180
180
  .applyDefaultPreset()
181
181
  .restoreSelection()
182
182
  .run();
@@ -6,18 +6,18 @@ Object {
6
6
  Object {
7
7
  "content": Array [
8
8
  Object {
9
+ "marks": Array [
10
+ Object {
11
+ "attrs": Object {
12
+ "value": "green",
13
+ },
14
+ "type": "background_color",
15
+ },
16
+ ],
9
17
  "text": "hello world",
10
18
  "type": "text",
11
19
  },
12
20
  ],
13
- "marks": Array [
14
- Object {
15
- "attrs": Object {
16
- "value": "green",
17
- },
18
- "type": "background_color",
19
- },
20
- ],
21
21
  "type": "paragraph",
22
22
  },
23
23
  ],
@@ -31,18 +31,18 @@ Object {
31
31
  Object {
32
32
  "content": Array [
33
33
  Object {
34
+ "marks": Array [
35
+ Object {
36
+ "attrs": Object {
37
+ "value": "#FF0000",
38
+ },
39
+ "type": "background_color",
40
+ },
41
+ ],
34
42
  "text": "test",
35
43
  "type": "text",
36
44
  },
37
45
  ],
38
- "marks": Array [
39
- Object {
40
- "attrs": Object {
41
- "value": "#FF0000",
42
- },
43
- "type": "background_color",
44
- },
45
- ],
46
46
  "type": "paragraph",
47
47
  },
48
48
  ],
@@ -126,18 +126,18 @@ Object {
126
126
  "type": "text",
127
127
  },
128
128
  Object {
129
+ "marks": Array [
130
+ Object {
131
+ "attrs": Object {
132
+ "value": "#FF0000",
133
+ },
134
+ "type": "background_color",
135
+ },
136
+ ],
129
137
  "text": " ipsum",
130
138
  "type": "text",
131
139
  },
132
140
  ],
133
- "marks": Array [
134
- Object {
135
- "attrs": Object {
136
- "value": "#FF0000",
137
- },
138
- "type": "background_color",
139
- },
140
- ],
141
141
  "type": "paragraph",
142
142
  },
143
143
  ],
@@ -145,4 +145,4 @@ Object {
145
145
  }
146
146
  `;
147
147
 
148
- exports[`rendering should render html 1`] = `"<span style="--zw-background-color:green;" class="zw-style"><p class="zw-style">hello world</p></span>"`;
148
+ exports[`rendering should render html 1`] = `"<p class="zw-style"><span style="--zw-background-color:green;" class="zw-style">hello world</span></p>"`;
@@ -66,7 +66,7 @@ export const NodeProcessor = Extension.create({
66
66
  return;
67
67
  }
68
68
 
69
- if (isNodeFullySelected($from, $to, node, position)) {
69
+ if (isNodeFullySelected(tr.doc, tr.selection, node, position)) {
70
70
  tr.step(new AddNodeMarkStep(position, applyingMark));
71
71
  }
72
72
  });
@@ -117,6 +117,17 @@ export const NodeProcessor = Extension.create({
117
117
  });
118
118
  }),
119
119
 
120
+ removeAllMarks: createCommand(({ state, commands }) => {
121
+ const { tr, doc } = state;
122
+ const { from, to } = tr.selection;
123
+
124
+ doc.nodesBetween(from, to, (node, position) => {
125
+ for (const mark of node.marks) {
126
+ commands._removeNodeMark({ tr, node, position, mark });
127
+ }
128
+ });
129
+ }),
130
+
120
131
  removeMarks: createCommand(({ state, commands }, marks) => {
121
132
  const { tr, doc } = state;
122
133
  const { from, to } = tr.selection;
@@ -1,7 +1,8 @@
1
1
  import { Node, wrappingInputRule } from '@tiptap/vue-2';
2
2
  import { computed, unref } from 'vue';
3
- import { createCommand } from '../../utils';
3
+ import { copyMark, createCommand } from '../../utils';
4
4
  import { ListTypes, MarkGroups, NodeTypes } from '../../enums';
5
+ import { AddNodeMarkStep } from '../core/steps';
5
6
  import { ListItem } from './ListItem';
6
7
 
7
8
  export const List = Node.create({
@@ -79,21 +80,37 @@ export const List = Node.create({
79
80
 
80
81
  // Remove List
81
82
  if (currentType === type) {
82
- commands.applyDefaultPreset();
83
+ commands.removeList();
83
84
  return;
84
85
  }
85
86
 
86
- return chain().applyDefaultPreset()._addList(type).run();
87
- }),
88
-
89
- _addList: createCommand(({ chain }, type) => {
90
87
  return chain()
88
+ .applyDefaultPreset()
91
89
  .toggleList(NodeTypes.LIST, NodeTypes.LIST_ITEM)
92
90
  .setBlockAttributes('bullet', { type })
93
91
  .run();
94
92
  }),
95
93
 
96
- removeList: createCommand(({ commands }) => {
94
+ removeList: createCommand(({ commands, state }) => {
95
+ const { tr, doc, selection } = state;
96
+ const from = selection.$from.start();
97
+ const to = selection.$to.end();
98
+
99
+ doc.nodesBetween(from, to, (node, position, parent) => {
100
+ if ([NodeTypes.LIST, NodeTypes.LIST_ITEM].includes(node.type.name)) return;
101
+ if (parent.type.name !== NodeTypes.LIST_ITEM) return false;
102
+
103
+ const addingMarks = parent.marks.filter(function (mark) {
104
+ return !mark.type.isInSet(node.marks);
105
+ });
106
+
107
+ for (const mark of addingMarks) {
108
+ tr.step(new AddNodeMarkStep(position, copyMark(mark)));
109
+ }
110
+
111
+ return false;
112
+ });
113
+
97
114
  commands.liftListItem(NodeTypes.LIST_ITEM);
98
115
  })
99
116
  };
@@ -1,10 +1,16 @@
1
- import { Editor } from '@tiptap/vue-2';
1
+ import { Editor, Mark } from '@tiptap/vue-2';
2
2
  import { buildTestExtensions } from '../../../__tests__/utils';
3
- import { ListTypes } from '../../../enums';
3
+ import { ListTypes, TextSettings } from '../../../enums';
4
4
  import { StylePreset } from '../../StylePreset';
5
5
  import { ContentNormalizer, NodeFactory } from '../../../services';
6
6
  import { List } from '../List';
7
7
 
8
+ const MockFontWeight = Mark.create({
9
+ name: TextSettings.FONT_WEIGHT,
10
+ renderHTML: () => ['span', {}, 0],
11
+ addAttributes: () => ({ value: { required: true } })
12
+ });
13
+
8
14
  function createEditor({ content }) {
9
15
  return new Editor({
10
16
  content: ContentNormalizer.normalize(content),
@@ -24,7 +30,8 @@ function createEditor({ content }) {
24
30
  ],
25
31
  defaultId: 'regular-1',
26
32
  baseClass: 'zw ts-'
27
- })
33
+ }),
34
+ MockFontWeight
28
35
  ]
29
36
  })
30
37
  });
@@ -73,7 +80,7 @@ describe('apply list', () => {
73
80
  expect(editor.getJSON()).toMatchSnapshot();
74
81
  });
75
82
 
76
- test('should remove list', () => {
83
+ test('should toggle list', () => {
77
84
  const editor = createEditor({
78
85
  content: NodeFactory.doc([
79
86
  NodeFactory.list(ListTypes.LATIN, [
@@ -84,12 +91,12 @@ describe('apply list', () => {
84
91
  });
85
92
 
86
93
  editor.commands.selectAll();
87
- editor.commands.removeList();
94
+ editor.commands.applyList(ListTypes.LATIN);
88
95
 
89
96
  expect(editor.getJSON()).toMatchSnapshot();
90
97
  });
91
98
 
92
- test('should toggle list', () => {
99
+ test('should change list type', () => {
93
100
  const editor = createEditor({
94
101
  content: NodeFactory.doc([
95
102
  NodeFactory.list(ListTypes.LATIN, [
@@ -100,37 +107,39 @@ describe('apply list', () => {
100
107
  });
101
108
 
102
109
  editor.commands.selectAll();
103
- editor.commands.applyList(ListTypes.LATIN);
110
+ editor.commands.applyList(ListTypes.ROMAN);
104
111
 
105
112
  expect(editor.getJSON()).toMatchSnapshot();
106
113
  });
107
114
 
108
- test('should change list type', () => {
115
+ test('should remove preset', () => {
109
116
  const editor = createEditor({
110
117
  content: NodeFactory.doc([
111
- NodeFactory.list(ListTypes.LATIN, [
112
- NodeFactory.paragraph('Item 1'),
113
- NodeFactory.paragraph('Item 2')
114
- ])
118
+ NodeFactory.paragraph({ preset: { id: 'regular-1' } }, 'Item 1'),
119
+ NodeFactory.paragraph({ preset: { id: 'regular-1' } }, 'Item 2')
115
120
  ])
116
121
  });
117
122
 
118
123
  editor.commands.selectAll();
119
- editor.commands.applyList(ListTypes.ROMAN);
124
+ editor.commands.applyList(ListTypes.LATIN);
120
125
 
121
126
  expect(editor.getJSON()).toMatchSnapshot();
122
127
  });
128
+ });
123
129
 
124
- test('should remove preset', () => {
130
+ describe('remove list', () => {
131
+ test('should remove list', () => {
125
132
  const editor = createEditor({
126
133
  content: NodeFactory.doc([
127
- NodeFactory.paragraph({ preset: { id: 'regular-1' } }, 'Item 1'),
128
- NodeFactory.paragraph({ preset: { id: 'regular-1' } }, 'Item 2')
134
+ NodeFactory.list(ListTypes.LATIN, [
135
+ NodeFactory.paragraph('Item 1'),
136
+ NodeFactory.paragraph('Item 2')
137
+ ])
129
138
  ])
130
139
  });
131
140
 
132
141
  editor.commands.selectAll();
133
- editor.commands.applyList(ListTypes.LATIN);
142
+ editor.commands.removeList();
134
143
 
135
144
  expect(editor.getJSON()).toMatchSnapshot();
136
145
  });
@@ -93,42 +93,6 @@ Object {
93
93
  }
94
94
  `;
95
95
 
96
- exports[`apply list should remove list 1`] = `
97
- Object {
98
- "content": Array [
99
- Object {
100
- "attrs": Object {
101
- "preset": Object {
102
- "id": "regular-1",
103
- },
104
- },
105
- "content": Array [
106
- Object {
107
- "text": "Item 1",
108
- "type": "text",
109
- },
110
- ],
111
- "type": "paragraph",
112
- },
113
- Object {
114
- "attrs": Object {
115
- "preset": Object {
116
- "id": "regular-1",
117
- },
118
- },
119
- "content": Array [
120
- Object {
121
- "text": "Item 2",
122
- "type": "text",
123
- },
124
- ],
125
- "type": "paragraph",
126
- },
127
- ],
128
- "type": "doc",
129
- }
130
- `;
131
-
132
96
  exports[`apply list should remove preset 1`] = `
133
97
  Object {
134
98
  "content": Array [
@@ -701,3 +665,39 @@ Object {
701
665
  "type": "doc",
702
666
  }
703
667
  `;
668
+
669
+ exports[`remove list should remove list 1`] = `
670
+ Object {
671
+ "content": Array [
672
+ Object {
673
+ "attrs": Object {
674
+ "preset": Object {
675
+ "id": "regular-1",
676
+ },
677
+ },
678
+ "content": Array [
679
+ Object {
680
+ "text": "Item 1",
681
+ "type": "text",
682
+ },
683
+ ],
684
+ "type": "paragraph",
685
+ },
686
+ Object {
687
+ "attrs": Object {
688
+ "preset": Object {
689
+ "id": "regular-1",
690
+ },
691
+ },
692
+ "content": Array [
693
+ Object {
694
+ "text": "Item 2",
695
+ "type": "text",
696
+ },
697
+ ],
698
+ "type": "paragraph",
699
+ },
700
+ ],
701
+ "type": "doc",
702
+ }
703
+ `;
@@ -34,6 +34,7 @@ export class HtmlNormalizer extends BaseNormalizer {
34
34
  this.#iterateNodes(this.#removeEmptyNodes, this.#isBlockNode);
35
35
  this.#iterateNodes(this.#normalizeListItems, (node) => node.tagName === 'LI');
36
36
  this.#normalizeBlockTextDecoration();
37
+ this.#normalizeBlockBackgroundColor();
37
38
  }
38
39
 
39
40
  get normalizedHTML() {
@@ -232,6 +233,28 @@ export class HtmlNormalizer extends BaseNormalizer {
232
233
  };
233
234
  }
234
235
 
236
+ #normalizeBlockBackgroundColor() {
237
+ const blockEls = this.dom.querySelectorAll('[style*="background-color"]:where(p, h1, h2, h3, h4, li)');
238
+
239
+ for (const blockEl of blockEls) {
240
+ this.#moveBackgroundColorToChildren(blockEl);
241
+ }
242
+ }
243
+
244
+ #moveBackgroundColorToChildren(blockEl) {
245
+ const blockColor = blockEl.style.backgroundColor;
246
+
247
+ blockEl.style.removeProperty('background-color');
248
+ if (!blockEl.style.cssText) blockEl.removeAttribute('style');
249
+
250
+ for (const childNode of blockEl.childNodes) {
251
+ const textEl = this.#wrapTextNode(blockEl, childNode);
252
+ const color = textEl.style.backgroundColor || blockColor;
253
+
254
+ textEl.style.backgroundColor = color;
255
+ }
256
+ }
257
+
235
258
  #wrapTextNode(parent, node) {
236
259
  if (node.nodeType !== this.#Node.TEXT_NODE) return node;
237
260
 
@@ -46,7 +46,7 @@ export class JsonNormalizer extends BaseNormalizer {
46
46
 
47
47
  for (const child of node.content) {
48
48
  if (!child.marks) return false;
49
- if (!this.#includesMarkType(child, childMark.type)) return false;
49
+ if (!this.#includesMark(child, childMark)) return false;
50
50
  }
51
51
 
52
52
  return true;
@@ -67,4 +67,19 @@ describe('normalize json content', () => {
67
67
 
68
68
  expect(ContentNormalizer.normalize(input)).toMatchSnapshot();
69
69
  });
70
+
71
+ test('should not marge mark with same type', () => {
72
+ const input = NodeFactory.doc([
73
+ NodeFactory.paragraph([
74
+ NodeFactory.text('lorem', [
75
+ NodeFactory.mark(TextSettings.FONT_WEIGHT, { value: '700' })
76
+ ]),
77
+ NodeFactory.text(' ipsum', [
78
+ NodeFactory.mark(TextSettings.FONT_WEIGHT, { value: '400' })
79
+ ])
80
+ ])
81
+ ]);
82
+
83
+ expect(ContentNormalizer.normalize(input)).toMatchSnapshot();
84
+ });
70
85
  });
@@ -157,3 +157,40 @@ Object {
157
157
  "type": "doc",
158
158
  }
159
159
  `;
160
+
161
+ exports[`normalize json content should not marge mark with same type 1`] = `
162
+ Object {
163
+ "content": Array [
164
+ Object {
165
+ "content": Array [
166
+ Object {
167
+ "marks": Array [
168
+ Object {
169
+ "attrs": Object {
170
+ "value": "700",
171
+ },
172
+ "type": "font_weight",
173
+ },
174
+ ],
175
+ "text": "lorem",
176
+ "type": "text",
177
+ },
178
+ Object {
179
+ "marks": Array [
180
+ Object {
181
+ "attrs": Object {
182
+ "value": "400",
183
+ },
184
+ "type": "font_weight",
185
+ },
186
+ ],
187
+ "text": " ipsum",
188
+ "type": "text",
189
+ },
190
+ ],
191
+ "type": "paragraph",
192
+ },
193
+ ],
194
+ "type": "doc",
195
+ }
196
+ `;
@@ -1,52 +1,43 @@
1
1
  import { isNodeFullySelected } from '../isNodeFullySelected';
2
2
 
3
- const createResolvedPosition = (attrs = {}) => ({
4
- pos: 0,
5
- path: [],
3
+ const createResolvedPosition = (attrs = {}) => ({ ...attrs });
4
+ const createSelection = (from, to) => ({ from, to });
5
+
6
+ const createNode = (attrs = {}) => ({
7
+ resolve: jest.fn(() => 0),
6
8
  ...attrs
7
9
  });
8
10
 
9
- const createNode = (attrs = {}) => ({ ...attrs });
10
-
11
11
  describe('is node fully selected', () => {
12
12
  test('should return false if selected part of text node', () => {
13
- const paragraph = createNode();
14
- const isSelected = isNodeFullySelected(
15
- createResolvedPosition({ pos: 7, path: [createNode(), 0, 0, paragraph, 0, 1] }),
16
- createResolvedPosition({ pos: 12, path: [createNode(), 0, 0, paragraph, 0, 1] }),
17
- paragraph,
18
- 0
19
- );
13
+ const doc = createNode();
14
+ const paragraph = createNode({ nodeSize: 245 });
15
+
16
+ doc.resolve.mockReturnValue(createResolvedPosition({ depth: 0 }));
17
+
18
+ const isSelected = isNodeFullySelected(doc, createSelection(7, 12), paragraph, 0);
20
19
 
21
20
  expect(isSelected).toBe(false);
22
21
  });
23
22
 
24
23
  test('should return true if selected paragraph node', () => {
24
+ const doc = createNode();
25
25
  const paragraph = createNode({ nodeSize: 245 });
26
- const isSelected = isNodeFullySelected(
27
- createResolvedPosition({ pos: 1, path: [createNode(), 0, 0, paragraph, 0, 1] }),
28
- createResolvedPosition({ pos: 244, path: [createNode(), 0, 0, paragraph, 0, 1] }),
29
- paragraph,
30
- 0
31
- );
26
+
27
+ doc.resolve.mockReturnValue(createResolvedPosition({ depth: 0 }));
28
+
29
+ const isSelected = isNodeFullySelected(doc, createSelection(1, 244), paragraph, 0);
32
30
 
33
31
  expect(isSelected).toBe(true);
34
32
  });
35
33
 
36
34
  test('should return true if selected list item node', () => {
35
+ const doc = createNode();
37
36
  const listItem = createNode({ nodeSize: 247 });
38
- const isSelected = isNodeFullySelected(
39
- createResolvedPosition({
40
- pos: 3,
41
- path: [createNode(), 0, 0, createNode(), 0, 1, listItem, 0, 2, createNode(), 0, 3]
42
- }),
43
- createResolvedPosition({
44
- pos: 246,
45
- path: [createNode(), 0, 0, createNode(), 0, 1, listItem, 0, 2, createNode(), 0, 3]
46
- }),
47
- listItem,
48
- 1
49
- );
37
+
38
+ doc.resolve.mockReturnValue(createResolvedPosition({ depth: 2 }));
39
+
40
+ const isSelected = isNodeFullySelected(doc, createSelection(3, 246), listItem, 1);
50
41
 
51
42
  expect(isSelected).toBe(true);
52
43
  });
@@ -0,0 +1,5 @@
1
+ import { cloneDeep } from 'lodash';
2
+
3
+ export function copyMark(mark) {
4
+ return mark.type.create(cloneDeep(mark.attrs));
5
+ }
@@ -12,3 +12,4 @@ export { resolveTextPosition } from './resolveTextPosition';
12
12
  export { isNodeFullySelected } from './isNodeFullySelected';
13
13
  export { isMarkAppliedToParent } from './isMarkAppliedToParent';
14
14
  export { findMarkByType } from './findMarkByType';
15
+ export { copyMark } from './copyMark';
@@ -1,14 +1,7 @@
1
- function resolveNodeTextOffset({ path }, node) {
2
- const nodes = path.filter((step) => typeof step === 'object');
3
- const index = nodes.indexOf(node);
4
-
5
- return nodes.slice(index).reverse().length;
6
- }
7
-
8
- export function isNodeFullySelected($from, $to, node, position) {
9
- const offset = resolveNodeTextOffset($from, node);
10
- const isFromMatch = $from.pos - offset <= position;
11
- const isToMatch = $to.pos + offset >= node.nodeSize + position;
1
+ export function isNodeFullySelected(doc, selection, node, position) {
2
+ const offset = doc.resolve(position).depth + 1;
3
+ const isFromMatch = selection.from - offset <= position;
4
+ const isToMatch = selection.to + offset >= node.nodeSize + position;
12
5
 
13
6
  return isFromMatch && isToMatch;
14
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zipify/wysiwyg",
3
- "version": "2.0.0-2",
3
+ "version": "2.0.0-4",
4
4
  "description": "Zipify modification of TipTap text editor",
5
5
  "main": "dist/wysiwyg.mjs",
6
6
  "bin": {