Skip to content

Commit 652f1db

Browse files
committed
perf: use custom element instead of span
1 parent 257cc6e commit 652f1db

File tree

4 files changed

+237
-253
lines changed

4 files changed

+237
-253
lines changed

src/EditorJSStyle.ts

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import type {
2+
API,
3+
InlineTool,
4+
InlineToolConstructorOptions,
5+
} from '@editorjs/editorjs';
6+
import EditorJSStyleElement from './EditorJSStyleElement';
7+
8+
class EditorJSStyle implements InlineTool {
9+
static get isInline() {
10+
return true;
11+
}
12+
13+
static get sanitize() {
14+
return {
15+
'editorjs-style': true,
16+
};
17+
}
18+
19+
static get title() {
20+
return 'Style';
21+
}
22+
23+
private actions: HTMLDivElement;
24+
private api: API;
25+
26+
constructor({ api }: InlineToolConstructorOptions) {
27+
this.actions = document.createElement('div');
28+
this.api = api;
29+
30+
if (customElements.get('editorjs-style')) {
31+
return;
32+
}
33+
34+
customElements.define('editorjs-style', EditorJSStyleElement);
35+
}
36+
37+
get shortcut() {
38+
return 'CMD+S';
39+
}
40+
41+
checkState() {
42+
this.actions.innerHTML = '';
43+
44+
const editorjsStyle = this.api.selection.findParentTag('EDITORJS-STYLE');
45+
46+
if (!editorjsStyle) {
47+
return false;
48+
}
49+
50+
this.actions.innerHTML = `
51+
<div style="margin-left: 0.5rem; ">
52+
<div style="display: flex; align-items: center; justify-content: space-between; ">
53+
<div>Style settings</div>
54+
55+
<button class="delete-button ${this.api.styles.settingsButton}" type="button">
56+
<svg class="icon" height="24" viewBox="0 0 24 24" width="24">
57+
<path d="M0 0h24v24H0z" fill="none"/>
58+
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
59+
</svg>
60+
</button>
61+
</div>
62+
63+
<label style="display: flex; align-items: center; justify-content: space-between; ">
64+
<span>ID</span>
65+
66+
<input class="id-input ${this.api.styles.input}" placeholder="exciting" style="width: 80%; ">
67+
</label>
68+
69+
<label style="display: flex; align-items: center; justify-content: space-between; ">
70+
<span>Class</span>
71+
72+
<input class="class-input ${this.api.styles.input}" placeholder="note editorial" style="width: 80%; ">
73+
</label>
74+
75+
<label style="display: flex; align-items: center; justify-content: space-between; ">
76+
<span>Style</span>
77+
78+
<textarea
79+
class="style-textarea ${this.api.styles.input}"
80+
placeholder="background: #ffe7e8;"
81+
style="resize: none; width: 80%; ">
82+
</textarea>
83+
</label>
84+
</div>
85+
`;
86+
87+
const deleteButton = this.actions.querySelector(
88+
'.delete-button'
89+
) as HTMLButtonElement | null;
90+
91+
const classInput = this.actions.querySelector(
92+
'.class-input'
93+
) as HTMLInputElement | null;
94+
95+
const idInput = this.actions.querySelector(
96+
'.id-input'
97+
) as HTMLInputElement | null;
98+
99+
const styleTextarea = this.actions.querySelector(
100+
'.style-textarea'
101+
) as HTMLTextAreaElement | null;
102+
103+
if (!deleteButton || !classInput || !idInput || !styleTextarea) {
104+
throw new Error("Couldn't render actions for editorjs-style. ");
105+
}
106+
107+
deleteButton.addEventListener('click', () => {
108+
const clonedNodes = Array.from(editorjsStyle.childNodes).map((node) =>
109+
node.cloneNode(true)
110+
);
111+
112+
clonedNodes.forEach((node) =>
113+
editorjsStyle.parentNode?.insertBefore(node, editorjsStyle)
114+
);
115+
editorjsStyle.remove();
116+
117+
if (clonedNodes.length === 0) {
118+
return;
119+
}
120+
121+
const selection = window.getSelection();
122+
123+
if (!selection) {
124+
throw new Error("Couldn't select unwrapped editorjs-style contents. ");
125+
}
126+
127+
selection.removeAllRanges();
128+
129+
const range = new Range();
130+
131+
range.setStartBefore(clonedNodes[0]);
132+
range.setEndAfter(clonedNodes[clonedNodes.length - 1]);
133+
134+
selection.addRange(range);
135+
});
136+
137+
this.api.tooltip.onHover(deleteButton, 'Delete style', {
138+
placement: 'top',
139+
});
140+
141+
classInput.value = editorjsStyle.className;
142+
143+
classInput.addEventListener('input', () =>
144+
editorjsStyle.setAttribute('class', classInput.value)
145+
);
146+
147+
idInput.value = editorjsStyle.id;
148+
149+
idInput.addEventListener('input', () => (editorjsStyle.id = idInput.value));
150+
151+
styleTextarea.value = editorjsStyle.getAttribute('style') ?? '';
152+
153+
// To input line breaks
154+
styleTextarea.addEventListener('keydown', (event) =>
155+
event.stopPropagation()
156+
);
157+
158+
styleTextarea.addEventListener('input', () =>
159+
editorjsStyle.setAttribute('style', styleTextarea.value)
160+
);
161+
162+
return true;
163+
}
164+
165+
clear() {
166+
this.actions.innerHTML = '';
167+
}
168+
169+
render() {
170+
const button = document.createElement('button');
171+
172+
button.classList.add(this.api.styles.inlineToolButton);
173+
button.type = 'button';
174+
175+
button.innerHTML = `
176+
<svg class="icon" height="24" viewBox="0 0 24 24" width="24">
177+
<path d="M0 0h24v24H0z" fill="none"/>
178+
<path d="M2.53 19.65l1.34.56v-9.03l-2.43 5.86c-.41 1.02.08 2.19 1.09 2.61zm19.5-3.7L17.07 3.98c-.31-.75-1.04-1.21-1.81-1.23-.26 0-.53.04-.79.15L7.1 5.95c-.75.31-1.21 1.03-1.23 1.8-.01.27.04.54.15.8l4.96 11.97c.31.76 1.05 1.22 1.83 1.23.26 0 .52-.05.77-.15l7.36-3.05c1.02-.42 1.51-1.59 1.09-2.6zM7.88 8.75c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-2 11c0 1.1.9 2 2 2h1.45l-3.45-8.34v6.34z"/>
179+
</svg>
180+
`;
181+
182+
return button;
183+
}
184+
185+
renderActions(): HTMLElement {
186+
return this.actions;
187+
}
188+
189+
surround(range: Range) {
190+
const editorjsStyle = new EditorJSStyleElement();
191+
192+
editorjsStyle.append(
193+
range.collapsed ? 'new style' : range.extractContents()
194+
);
195+
196+
setTimeout(() => {
197+
range.insertNode(editorjsStyle);
198+
this.api.selection.expandToTag(editorjsStyle);
199+
});
200+
}
201+
}
202+
203+
export default EditorJSStyle;

src/EditorJSStyleElement.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class EditorJSStyleElement extends HTMLElement {
2+
constructor() {
3+
super();
4+
5+
// To prevent Editor.js keydown event
6+
this.addEventListener('keydown', (event) => event.stopPropagation());
7+
8+
const mutationObserver = new MutationObserver(() => {
9+
if (
10+
this.firstChild?.nodeName !== '#text' ||
11+
this.firstChild?.textContent?.slice(0, 1) !== '\u200b'
12+
) {
13+
this.prepend('\u200b');
14+
}
15+
16+
if (
17+
this.lastChild?.nodeName !== '#text' ||
18+
this.lastChild?.textContent?.slice(-1) !== '\u200b'
19+
) {
20+
this.append('\u200b');
21+
}
22+
});
23+
24+
mutationObserver.observe(this, {
25+
characterData: true,
26+
childList: true,
27+
subtree: true,
28+
});
29+
}
30+
}
31+
32+
export default EditorJSStyleElement;

0 commit comments

Comments
 (0)