@kubex/zinc 1.1.165 → 1.1.168
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/dist/custom-elements.json +98 -74
- package/dist/vscode.html-custom-data.json +11 -11
- package/dist/web-types.json +23 -23
- package/dist/zn.d.ts +27 -0
- package/dist/zn.min.js +599 -570
- package/docs/pages/components/remarkd-editor.md +1 -1
- package/package.json +1 -1
- package/src/components/remarkd-editor/actions.ts +2 -2
- package/src/components/remarkd-editor/list-continuation.test.ts +77 -1
- package/src/components/remarkd-editor/list-continuation.ts +117 -16
- package/src/components/remarkd-editor/paste.test.ts +119 -0
- package/src/components/remarkd-editor/paste.ts +241 -0
- package/src/components/remarkd-editor/remarkd-editor.component.ts +38 -5
- package/src/components/remarkd-editor/remarkd-editor.test.ts +85 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {ACTION_GROUPS, EDITOR_ACTIONS, matchesShortcut, shortcutLabel, slashItems} from './actions';
|
|
2
2
|
import {classMap} from "lit/directives/class-map.js";
|
|
3
|
-
import {continueList, isEmptyListItem} from './list-continuation';
|
|
3
|
+
import {continueList, indentList, isEmptyListItem} from './list-continuation';
|
|
4
4
|
import {type CSSResultGroup, html, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
|
|
5
5
|
import {defaultValue} from "../../internal/default-value";
|
|
6
6
|
import {FormControlController} from "../../internal/form";
|
|
7
|
+
import {pastedRemarkd} from './paste';
|
|
7
8
|
import {property, query, state} from 'lit/decorators.js';
|
|
8
9
|
import {parse as remarkdParse} from "remarkd-js";
|
|
9
10
|
import {ToolbarOverflowController} from '../../internal/toolbar-overflow';
|
|
@@ -15,6 +16,7 @@ import ZnMenu from "../menu";
|
|
|
15
16
|
import ZnMenuItem from "../menu-item";
|
|
16
17
|
import ZnSlashMenu, {SlashMenuController, type SlashMenuItem} from "../slash-menu";
|
|
17
18
|
import type {EditorAction, InlineMark} from './actions';
|
|
19
|
+
import type {RangeEdit} from './list-continuation';
|
|
18
20
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
19
21
|
import type ZnFile from "../file";
|
|
20
22
|
|
|
@@ -978,6 +980,11 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
978
980
|
if (!edit) return;
|
|
979
981
|
e.preventDefault();
|
|
980
982
|
this.applyDraftEdit(input, edit.value, edit.caret, edit.caret);
|
|
983
|
+
} else if (this.isListTab(e)) {
|
|
984
|
+
const edit = indentList(input.value, input.selectionStart, input.selectionEnd, e.shiftKey);
|
|
985
|
+
if (!edit) return;
|
|
986
|
+
e.preventDefault();
|
|
987
|
+
this.applyDraftEdit(input, edit.value, edit.start, edit.end);
|
|
981
988
|
} else if (e.key === 'Enter' && e.shiftKey) {
|
|
982
989
|
e.preventDefault();
|
|
983
990
|
this.suppressBlurCommit = true;
|
|
@@ -1010,14 +1017,23 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1010
1017
|
return e.key === 'Enter' && !e.isComposing && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey;
|
|
1011
1018
|
}
|
|
1012
1019
|
|
|
1020
|
+
private isListTab(e: KeyboardEvent): boolean {
|
|
1021
|
+
return e.key === 'Tab' && !e.isComposing && !e.metaKey && !e.ctrlKey && !e.altKey;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1013
1024
|
private handleRawKeydown = (e: KeyboardEvent) => {
|
|
1014
|
-
if (!this.isPlainEnter(e)) return;
|
|
1015
1025
|
const input = e.target as HTMLTextAreaElement;
|
|
1016
|
-
|
|
1026
|
+
let edit: RangeEdit | null = null;
|
|
1027
|
+
if (this.isPlainEnter(e)) {
|
|
1028
|
+
const next = continueList(input.value, input.selectionStart, input.selectionEnd);
|
|
1029
|
+
edit = next && {value: next.value, start: next.caret, end: next.caret};
|
|
1030
|
+
} else if (this.isListTab(e)) {
|
|
1031
|
+
edit = indentList(input.value, input.selectionStart, input.selectionEnd, e.shiftKey);
|
|
1032
|
+
}
|
|
1017
1033
|
if (!edit) return;
|
|
1018
1034
|
e.preventDefault();
|
|
1019
1035
|
input.value = edit.value;
|
|
1020
|
-
input.setSelectionRange(edit.
|
|
1036
|
+
input.setSelectionRange(edit.start, edit.end);
|
|
1021
1037
|
this.handleRawInput(e);
|
|
1022
1038
|
};
|
|
1023
1039
|
|
|
@@ -1061,12 +1077,28 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1061
1077
|
|
|
1062
1078
|
private handleEditPaste = (e: ClipboardEvent) => {
|
|
1063
1079
|
const file = Array.from(e.clipboardData?.files ?? []).find(f => f.type.startsWith('image/'));
|
|
1064
|
-
if (!file)
|
|
1080
|
+
if (!file) {
|
|
1081
|
+
this.pasteFormatted(e);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1065
1084
|
e.preventDefault();
|
|
1066
1085
|
const index = this.editingIndex ?? this.blocks.length;
|
|
1067
1086
|
void this.insertImage(file, index + 1);
|
|
1068
1087
|
};
|
|
1069
1088
|
|
|
1089
|
+
/** Pastes rich clipboard content as remarkd source, keeping its lists and formatting. */
|
|
1090
|
+
private pasteFormatted = (e: ClipboardEvent) => {
|
|
1091
|
+
const source = pastedRemarkd(e.clipboardData);
|
|
1092
|
+
if (source === null) return;
|
|
1093
|
+
e.preventDefault();
|
|
1094
|
+
const input = e.target as HTMLTextAreaElement;
|
|
1095
|
+
// insertText keeps the paste on the native undo stack; it fires `input` like a typed paste.
|
|
1096
|
+
if (!document.execCommand('insertText', false, source)) {
|
|
1097
|
+
input.setRangeText(source, input.selectionStart, input.selectionEnd, 'end');
|
|
1098
|
+
input.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1070
1102
|
private handleDragOver = (e: DragEvent) => {
|
|
1071
1103
|
if (this.rawMode) return;
|
|
1072
1104
|
if (e.dataTransfer?.types.includes('Files')) e.preventDefault();
|
|
@@ -2006,6 +2038,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
2006
2038
|
.value=${this.value}
|
|
2007
2039
|
@input=${this.handleRawInput}
|
|
2008
2040
|
@keydown=${this.handleRawKeydown}
|
|
2041
|
+
@paste=${this.pasteFormatted}
|
|
2009
2042
|
@blur=${this.commitRaw}></textarea>`;
|
|
2010
2043
|
}
|
|
2011
2044
|
|
|
@@ -649,6 +649,90 @@ Second"></zn-remarkd-editor>`);
|
|
|
649
649
|
await el.updateComplete;
|
|
650
650
|
expect(el.value).to.equal('- [ ] first\n- [ ] ');
|
|
651
651
|
});
|
|
652
|
+
|
|
653
|
+
const tab = (input: HTMLTextAreaElement, shiftKey = false) => {
|
|
654
|
+
const event = new KeyboardEvent('keydown', {key: 'Tab', shiftKey, bubbles: true, composed: true, cancelable: true});
|
|
655
|
+
input.dispatchEvent(event);
|
|
656
|
+
return event;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
it('should nest and un-nest a list item on Tab and Shift+Tab in the open block', async () => {
|
|
660
|
+
const {el, input} = await editDraft('- a\n- b');
|
|
661
|
+
expect(tab(input).defaultPrevented).to.be.true;
|
|
662
|
+
await el.updateComplete;
|
|
663
|
+
expect(input.value).to.equal('- a\n-- b');
|
|
664
|
+
expect(input.selectionStart).to.equal(8);
|
|
665
|
+
|
|
666
|
+
expect(tab(input, true).defaultPrevented).to.be.true;
|
|
667
|
+
await el.updateComplete;
|
|
668
|
+
expect(input.value).to.equal('- a\n- b');
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('should leave Tab alone outside a list', async () => {
|
|
672
|
+
const {input} = await editDraft('hello');
|
|
673
|
+
expect(tab(input).defaultPrevented).to.be.false;
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('should nest a list item on Tab in the raw source view', async () => {
|
|
677
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
678
|
+
<zn-remarkd-editor allow-raw value="- first"></zn-remarkd-editor>`);
|
|
679
|
+
el.shadowRoot!.querySelector('.remarkd-editor__raw-toggle')!
|
|
680
|
+
.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
|
|
681
|
+
await el.updateComplete;
|
|
682
|
+
const raw = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__raw')!;
|
|
683
|
+
raw.setSelectionRange(7, 7);
|
|
684
|
+
|
|
685
|
+
expect(tab(raw).defaultPrevented).to.be.true;
|
|
686
|
+
await el.updateComplete;
|
|
687
|
+
expect(el.value).to.equal('-- first');
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
describe('paste', () => {
|
|
692
|
+
const paste = (input: HTMLTextAreaElement, data: Record<string, string>) => {
|
|
693
|
+
const clipboardData = new DataTransfer();
|
|
694
|
+
for (const [type, value] of Object.entries(data)) clipboardData.setData(type, value);
|
|
695
|
+
const event = new ClipboardEvent('paste', {clipboardData, bubbles: true, composed: true, cancelable: true});
|
|
696
|
+
input.dispatchEvent(event);
|
|
697
|
+
return event;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
it('should paste rich content as remarkd source at the caret', async () => {
|
|
701
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
702
|
+
<zn-remarkd-editor value="Intro"></zn-remarkd-editor>`);
|
|
703
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
704
|
+
await el.updateComplete;
|
|
705
|
+
const input = typeInBlock(el, 'Intro\n');
|
|
706
|
+
input.focus();
|
|
707
|
+
|
|
708
|
+
const event = paste(input, {'text/html': '<ol><li>a<ol><li>b</li></ol></li></ol>', 'text/plain': 'a\nb'});
|
|
709
|
+
expect(event.defaultPrevented).to.be.true;
|
|
710
|
+
await el.updateComplete;
|
|
711
|
+
expect(input.value).to.equal('Intro\n1. a\n1.1. b');
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should leave a plain-text paste to the browser', async () => {
|
|
715
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
716
|
+
<zn-remarkd-editor value="Intro"></zn-remarkd-editor>`);
|
|
717
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
718
|
+
await el.updateComplete;
|
|
719
|
+
const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
|
|
720
|
+
expect(paste(input, {'text/plain': 'just text'}).defaultPrevented).to.be.false;
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should paste rich content in the raw source view', async () => {
|
|
724
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
725
|
+
<zn-remarkd-editor allow-raw value=""></zn-remarkd-editor>`);
|
|
726
|
+
el.shadowRoot!.querySelector('.remarkd-editor__raw-toggle')!
|
|
727
|
+
.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
|
|
728
|
+
await el.updateComplete;
|
|
729
|
+
const raw = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__raw')!;
|
|
730
|
+
raw.focus();
|
|
731
|
+
|
|
732
|
+
expect(paste(raw, {'text/html': '<ul><li>a</li></ul>'}).defaultPrevented).to.be.true;
|
|
733
|
+
await el.updateComplete;
|
|
734
|
+
expect(el.value).to.equal('- a');
|
|
735
|
+
});
|
|
652
736
|
});
|
|
653
737
|
|
|
654
738
|
it('should insert a placeholder when nothing is selected', async () => {
|
|
@@ -915,7 +999,7 @@ Second"></zn-remarkd-editor>`);
|
|
|
915
999
|
|
|
916
1000
|
const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
|
|
917
1001
|
expect(menu.items.length).to.equal(EDITOR_ACTIONS.length);
|
|
918
|
-
expect(menu.items.some(item => item.group === '
|
|
1002
|
+
expect(menu.items.some(item => item.group === 'Alerts')).to.be.true;
|
|
919
1003
|
});
|
|
920
1004
|
|
|
921
1005
|
it('should omit the Include action from the slash menu without an include-url', async () => {
|