@dile/editor 3.0.18 → 3.0.19

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.
package/editor.js CHANGED
@@ -1,3 +1,5 @@
1
1
  import { DileEditor } from "./src/DileEditor.js";
2
2
 
3
- window.customElements.define("dile-editor", DileEditor);
3
+ if (!customElements.get("dile-editor")) {
4
+ window.customElements.define("dile-editor", DileEditor);
5
+ }
package/editor.spec.js ADDED
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './editor.js';
3
+
4
+ describe('dile-editor', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = '';
7
+ });
8
+
9
+ function waitFor(fn, timeout = 5000) {
10
+ return new Promise((resolve, reject) => {
11
+ const start = Date.now();
12
+ const check = () => {
13
+ const result = fn();
14
+ if (result) return resolve(result);
15
+ if (Date.now() - start > timeout) return reject(new Error('waitFor timed out'));
16
+ setTimeout(check, 20);
17
+ };
18
+ check();
19
+ });
20
+ }
21
+
22
+ async function renderEditor(html = '<dile-editor></dile-editor>') {
23
+ document.body.innerHTML = html;
24
+ const el = document.body.querySelector('dile-editor');
25
+ await el.updateComplete;
26
+ await waitFor(() => el.shadowRoot.querySelector('dile-editor-markdown .ProseMirror'));
27
+ return el;
28
+ }
29
+
30
+ it('renders the view tabs and the markdown editor with its toolbar', async () => {
31
+ const el = await renderEditor();
32
+
33
+ expect(el.shadowRoot.querySelector('dile-tabs')).toBeTruthy();
34
+ expect(el.shadowRoot.querySelector('dile-pages')).toBeTruthy();
35
+
36
+ const markdownEditor = el.shadowRoot.querySelector('dile-editor-markdown');
37
+ expect(markdownEditor.querySelector('dile-editor-toolbar')).toBeTruthy();
38
+
39
+ const content = markdownEditor.querySelector('.ProseMirror');
40
+ expect(content.getAttribute('contenteditable')).toBe('true');
41
+ });
42
+
43
+ it('renders a label when the label property is set', async () => {
44
+ const el = await renderEditor('<dile-editor label="My editor"></dile-editor>');
45
+ expect(el.shadowRoot.querySelector('label').textContent).toBe('My editor');
46
+ });
47
+
48
+ it('reflects typed content back through the value property', async () => {
49
+ const el = await renderEditor();
50
+
51
+ el.updateValue({ detail: { content: 'Hello world' } });
52
+ await el.updateComplete;
53
+
54
+ expect(el.value).toBe('Hello world');
55
+ });
56
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dile/editor",
3
- "version": "3.0.18",
3
+ "version": "3.0.19",
4
4
  "description": "Flexible Web Component for rich text editing with WYSIWYG and Markdown support. Easy to integrate, customizable UI, powered by ProseMirror.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@dile/icons": "^2.5.1",
34
- "@dile/ui": "^3.4.5",
34
+ "@dile/ui": "^3.4.6",
35
35
  "lit": "^2.7.0 || ^3.0.0",
36
36
  "prosemirror-commands": "^1.6.2",
37
37
  "prosemirror-example-setup": "^1.2.3",
@@ -42,5 +42,5 @@
42
42
  "prosemirror-state": "^1.4.3",
43
43
  "prosemirror-view": "^1.37.0"
44
44
  },
45
- "gitHead": "31136286663cc7482a95a8813d845e73c3f6c57d"
45
+ "gitHead": "57a54e50de1a24d64dcf478fa6d39624e1f775eb"
46
46
  }
@@ -74,4 +74,6 @@ export class DileEditorImageDialog extends DileI18nMixin(LitElement) {
74
74
  this.altInput.value = '';
75
75
  }
76
76
  }
77
- customElements.define('dile-editor-image-dialog', DileEditorImageDialog);
77
+ if (!customElements.get('dile-editor-image-dialog')) {
78
+ customElements.define('dile-editor-image-dialog', DileEditorImageDialog);
79
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './dile-editor-image-dialog.js';
3
+
4
+ describe('dile-editor-image-dialog', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = '';
7
+ });
8
+
9
+ async function renderDialog() {
10
+ document.body.innerHTML = '<dile-editor-image-dialog></dile-editor-image-dialog>';
11
+ const el = document.body.querySelector('dile-editor-image-dialog');
12
+ await el.updateComplete;
13
+ return el;
14
+ }
15
+
16
+ it('renders the src and alt inputs', async () => {
17
+ const el = await renderDialog();
18
+ expect(el.srcInput).toBeTruthy();
19
+ expect(el.altInput).toBeTruthy();
20
+ });
21
+
22
+ it('opens the menu overlay', async () => {
23
+ const el = await renderDialog();
24
+ let opened = false;
25
+ el.menu.addEventListener('overlay-opened', () => { opened = true; });
26
+
27
+ el.open({ state: { selection: {} } });
28
+
29
+ expect(opened).toBe(true);
30
+ });
31
+
32
+ it('dispatches accept-image-dialog with the entered src and alt', async () => {
33
+ const el = await renderDialog();
34
+ el.srcInput.value = 'https://example.com/img.png';
35
+ el.altInput.value = 'An image';
36
+
37
+ let detail = null;
38
+ el.addEventListener('accept-image-dialog', (e) => { detail = e.detail; });
39
+ el.accept();
40
+
41
+ expect(detail).toEqual({ src: 'https://example.com/img.png', alt: 'An image' });
42
+ });
43
+ });
@@ -75,4 +75,6 @@ export class DileEditorLinkDialog extends DileI18nMixin(LitElement) {
75
75
  this.titleInput.value = '';
76
76
  }
77
77
  }
78
- customElements.define('dile-editor-link-dialog', DileEditorLinkDialog);
78
+ if (!customElements.get('dile-editor-link-dialog')) {
79
+ customElements.define('dile-editor-link-dialog', DileEditorLinkDialog);
80
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './dile-editor-link-dialog.js';
3
+
4
+ describe('dile-editor-link-dialog', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = '';
7
+ });
8
+
9
+ async function renderDialog() {
10
+ document.body.innerHTML = '<dile-editor-link-dialog></dile-editor-link-dialog>';
11
+ const el = document.body.querySelector('dile-editor-link-dialog');
12
+ await el.updateComplete;
13
+ return el;
14
+ }
15
+
16
+ it('renders the url and title inputs', async () => {
17
+ const el = await renderDialog();
18
+ expect(el.urlInput).toBeTruthy();
19
+ expect(el.titleInput).toBeTruthy();
20
+ });
21
+
22
+ it('opens and closes the menu overlay', async () => {
23
+ const el = await renderDialog();
24
+ let opened = false;
25
+ let closed = false;
26
+ el.menu.addEventListener('overlay-opened', () => { opened = true; });
27
+ el.menu.addEventListener('overlay-closed', () => { closed = true; });
28
+
29
+ el.open();
30
+ expect(opened).toBe(true);
31
+
32
+ el.close();
33
+ expect(closed).toBe(true);
34
+ });
35
+
36
+ it('dispatches accept-link-dialog with the entered url and title', async () => {
37
+ const el = await renderDialog();
38
+ el.urlInput.value = 'https://example.com';
39
+ el.titleInput.value = 'Example';
40
+
41
+ let detail = null;
42
+ el.addEventListener('accept-link-dialog', (e) => { detail = e.detail; });
43
+ el.accept();
44
+
45
+ expect(detail).toEqual({ url: 'https://example.com', title: 'Example' });
46
+ });
47
+ });
@@ -96,4 +96,6 @@ export class DileEditorMarkdown extends DileI18nMixin(LitElement) {
96
96
  this.view.focus();
97
97
  }
98
98
  }
99
- customElements.define('dile-editor-markdown', DileEditorMarkdown);
99
+ if (!customElements.get('dile-editor-markdown')) {
100
+ customElements.define('dile-editor-markdown', DileEditorMarkdown);
101
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './dile-editor-markdown.js';
3
+ import { defaultToolbarConfig } from './defaultToolbarConfig.js';
4
+
5
+ describe('dile-editor-markdown', () => {
6
+ afterEach(() => {
7
+ document.body.innerHTML = '';
8
+ });
9
+
10
+ function waitForEvent(el, eventName) {
11
+ return new Promise((resolve) => {
12
+ el.addEventListener(eventName, (e) => resolve(e), { once: true });
13
+ });
14
+ }
15
+
16
+ async function renderMarkdownEditor() {
17
+ document.body.innerHTML = '<dile-editor-markdown></dile-editor-markdown>';
18
+ const el = document.body.querySelector('dile-editor-markdown');
19
+ el._menuConfig = { ...defaultToolbarConfig };
20
+ el.additionalCommands = {};
21
+ await waitForEvent(el, 'dile-editor-markdown-initialized');
22
+ await el.updateComplete;
23
+ return el;
24
+ }
25
+
26
+ it('renders a contenteditable ProseMirror surface and a toolbar', async () => {
27
+ const el = await renderMarkdownEditor();
28
+ const content = el.querySelector('.ProseMirror');
29
+ expect(content).toBeTruthy();
30
+ expect(content.getAttribute('contenteditable')).toBe('true');
31
+ expect(el.querySelector('dile-editor-toolbar')).toBeTruthy();
32
+ });
33
+
34
+ it('serializes empty content as an empty string', async () => {
35
+ const el = await renderMarkdownEditor();
36
+ expect(el.editorMarkdown).toBe('');
37
+ });
38
+
39
+ it('updates the ProseMirror document when content is set', async () => {
40
+ const el = await renderMarkdownEditor();
41
+ el.updateEditorContent('Hello world');
42
+ expect(el.editorMarkdown).toBe('Hello world');
43
+ });
44
+ });
@@ -56,4 +56,6 @@ export class DileEditorToolbarItem extends DileI18nMixin(LitElement) {
56
56
  }
57
57
  }
58
58
  }
59
- customElements.define('dile-editor-toolbar-item', DileEditorToolbarItem);
59
+ if (!customElements.get('dile-editor-toolbar-item')) {
60
+ customElements.define('dile-editor-toolbar-item', DileEditorToolbarItem);
61
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './dile-editor-toolbar-item.js';
3
+
4
+ describe('dile-editor-toolbar-item', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = '';
7
+ });
8
+
9
+ async function renderItem(item, active = false) {
10
+ document.body.innerHTML = '<dile-editor-toolbar-item></dile-editor-toolbar-item>';
11
+ const el = document.body.querySelector('dile-editor-toolbar-item');
12
+ el.item = item;
13
+ el.active = active;
14
+ await el.updateComplete;
15
+ return el;
16
+ }
17
+
18
+ it('renders the icon for the given item', async () => {
19
+ const el = await renderItem({ icon: '<svg></svg>', commandName: 'bold' });
20
+ expect(el.shadowRoot.querySelector('dile-icon')).toBeTruthy();
21
+ });
22
+
23
+ it('applies the active class when active is true', async () => {
24
+ const el = await renderItem({ icon: '<svg></svg>', commandName: 'bold' }, true);
25
+ expect(el.shadowRoot.querySelector('dile-icon').classList.contains('active')).toBe(true);
26
+ });
27
+
28
+ it('dispatches dile-toolbar-command when active and clicked', async () => {
29
+ const el = await renderItem({ icon: '<svg></svg>', commandName: 'bold' }, true);
30
+ let detail = null;
31
+ el.addEventListener('dile-toolbar-command', (e) => { detail = e.detail; });
32
+
33
+ el.shadowRoot.querySelector('dile-icon').click();
34
+
35
+ expect(detail.item.commandName).toBe('bold');
36
+ });
37
+ });
@@ -179,4 +179,6 @@ export class DileEditorToolbar extends DileI18nMixin(LitElement) {
179
179
  this.editorView.focus();
180
180
  }
181
181
  }
182
- customElements.define('dile-editor-toolbar', DileEditorToolbar);
182
+ if (!customElements.get('dile-editor-toolbar')) {
183
+ customElements.define('dile-editor-toolbar', DileEditorToolbar);
184
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './dile-editor-toolbar.js';
3
+ import { defaultToolbarConfig } from './defaultToolbarConfig.js';
4
+
5
+ describe('dile-editor-toolbar', () => {
6
+ afterEach(() => {
7
+ document.body.innerHTML = '';
8
+ });
9
+
10
+ function fakeEditorView() {
11
+ return {
12
+ state: {},
13
+ dispatch: () => {},
14
+ focus: () => {},
15
+ };
16
+ }
17
+
18
+ async function renderToolbar() {
19
+ document.body.innerHTML = '<dile-editor-toolbar></dile-editor-toolbar>';
20
+ const el = document.body.querySelector('dile-editor-toolbar');
21
+ el.menuConfig = { ...defaultToolbarConfig };
22
+ el.editorView = fakeEditorView();
23
+ await el.updateComplete;
24
+ return el;
25
+ }
26
+
27
+ it('renders a toolbar item for each enabled mark command', async () => {
28
+ const el = await renderToolbar();
29
+ expect(el.shadowRoot.querySelectorAll('dile-editor-toolbar-item').length).toBeGreaterThan(0);
30
+ });
31
+
32
+ it('renders the block type select', async () => {
33
+ const el = await renderToolbar();
34
+ expect(el.shadowRoot.getElementById('blockselect')).toBeTruthy();
35
+ });
36
+
37
+ it('does not render an item for a command disabled in menuConfig', async () => {
38
+ document.body.innerHTML = '<dile-editor-toolbar></dile-editor-toolbar>';
39
+ const el = document.body.querySelector('dile-editor-toolbar');
40
+ el.menuConfig = { ...defaultToolbarConfig, bold: false };
41
+ el.editorView = fakeEditorView();
42
+ await el.updateComplete;
43
+
44
+ const items = [...el.shadowRoot.querySelectorAll('dile-editor-toolbar-item')];
45
+ expect(items.some(item => item.item.commandName === 'bold')).toBe(false);
46
+ });
47
+ });