Skip to content

Commit e4f4e39

Browse files
committed
docs(text-editor): Add example for custom trigger
1 parent 74b4a70 commit e4f4e39

File tree

3 files changed

+293
-1
lines changed

3 files changed

+293
-1
lines changed

src/components/text-editor/examples/text-editor-custom-element.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ export class TextEditorCustomElementExample {
2525
value={this.value}
2626
onChange={this.handleChange}
2727
plugins={[
28-
{ tagName: 'limel-chip', attributes: ['text', 'icon'] },
28+
{
29+
tagName: 'limel-chip',
30+
attributes: ['text', 'icon'],
31+
},
2932
]}
3033
/>,
3134
<limel-example-value value={this.value} />,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
limel-button-group {
2+
min-width: 8rem;
3+
}
4+
5+
limel-example-controls {
6+
display: flex;
7+
flex-wrap: wrap;
8+
}
9+
10+
.mode {
11+
display: flex;
12+
flex-wrap: nowrap;
13+
}
14+
15+
.value {
16+
display: flex;
17+
gap: 0.5rem;
18+
}
19+
20+
limel-portal {
21+
width: auto;
22+
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import { Button, ListItem } from '@limetech/lime-elements';
2+
import {
3+
Component,
4+
h,
5+
State,
6+
Element,
7+
Event,
8+
EventEmitter,
9+
Watch,
10+
} from '@stencil/core';
11+
import { createRandomString } from 'src/util/random-string';
12+
import { portalContains } from '../../portal/contains';
13+
import { ESCAPE } from '../../../util/keycodes';
14+
import {
15+
TriggerEventDetail,
16+
TextEditor,
17+
} from '../prosemirror-adapter/plugins/trigger-plugin';
18+
19+
/**
20+
* Custom triggers
21+
*
22+
* A trigger is a character or sequence of characters that if typed in the text editor
23+
* will initiate a trigger session. The session is initialized with a `triggerStart`
24+
* event. Subsequent characters written after the trigger sequence will be sent in a
25+
* `triggerChange` event. When the focus is removed from the trigger a `triggerStop`
26+
* event will be sent.
27+
*
28+
* The `triggerStart` event contains a `TextEditorInserter` object containing functions
29+
* to manipulate the state of the text editor around the trigger. Using any of the
30+
* supplied methods will effectivly replace the trigger content in the text editor with
31+
* the content of choice.
32+
*
33+
* In this example we pass either a text or a `limel-chip` representing some chosen user
34+
* in a mention like situation.
35+
*/
36+
@Component({
37+
tag: 'limel-example-text-editor-triggers',
38+
shadow: true,
39+
styleUrl: 'text-editor-custom-triggers.scss',
40+
})
41+
export class TextEditorCustomTriggersExample {
42+
constructor() {
43+
this.portalId = createRandomString();
44+
this.globalClickListener = this.globalClickListener.bind(this);
45+
}
46+
@State()
47+
private value: string = '';
48+
49+
@State()
50+
private triggerState: string = '';
51+
52+
@State()
53+
private inputText: string = '';
54+
55+
@State()
56+
private isPickerOpen: boolean = false;
57+
58+
@State()
59+
private textEditorElement: HTMLElement;
60+
61+
@State()
62+
private insertMode: 'text' | 'chip' = 'text';
63+
64+
@Element()
65+
private host: HTMLLimelPopoverElement;
66+
67+
/**
68+
* Emits an event when the component is closing
69+
*/
70+
@Event()
71+
private close: EventEmitter<void>;
72+
73+
@Watch('isPickerOpen')
74+
protected watchOpen() {
75+
this.setupGlobalHandlers();
76+
}
77+
78+
public componentWillLoad() {
79+
this.setupGlobalHandlers();
80+
}
81+
82+
private setupGlobalHandlers() {
83+
if (this.isPickerOpen) {
84+
document.addEventListener('click', this.globalClickListener, {
85+
capture: true,
86+
});
87+
document.addEventListener('keyup', this.handleGlobalKeyPress);
88+
} else {
89+
document.removeEventListener('click', this.globalClickListener);
90+
document.removeEventListener('keyup', this.handleGlobalKeyPress);
91+
}
92+
}
93+
94+
private handleGlobalKeyPress = (event: KeyboardEvent) => {
95+
if (event.key !== ESCAPE) {
96+
return;
97+
}
98+
99+
event.stopPropagation();
100+
event.preventDefault();
101+
this.close.emit();
102+
};
103+
104+
private insertModeButtons: Button[] = [
105+
{
106+
id: '1',
107+
title: 'text',
108+
selected: true,
109+
},
110+
{
111+
id: '2',
112+
title: 'chip',
113+
},
114+
];
115+
116+
private portalId: string;
117+
private items: Array<ListItem<number>> = [
118+
{ text: 'Wolverine', value: 1, icon: 'wolf' },
119+
{ text: 'Captain America', value: 2, icon: 'captain_america' },
120+
{ text: 'Superman', value: 3, icon: 'superman' },
121+
{ text: 'Tony Stark', value: 4, icon: 'iron_man' },
122+
{ text: 'Batman', value: 5, icon: 'batman_old' },
123+
];
124+
125+
private triggerFunction?: TextEditor;
126+
127+
public render() {
128+
return [
129+
<limel-text-editor
130+
style={{ display: 'block' }}
131+
ref={(el) => (this.textEditorElement = el)}
132+
value={this.value}
133+
plugins={[
134+
{ tagName: 'limel-chip', attributes: ['text', 'icon'] },
135+
]}
136+
triggers={['@']}
137+
onTriggerStart={this.handleTriggerStart}
138+
onTriggerStop={this.handleTriggerStop}
139+
onTriggerChange={this.handleTriggerChange}
140+
onChange={this.handleChange}
141+
/>,
142+
<limel-example-controls>
143+
Insert mode:
144+
<limel-button-group
145+
class="mode"
146+
value={this.insertModeButtons}
147+
onChange={this.handleInsertModeChange}
148+
/>
149+
<div class="value">
150+
<limel-example-value
151+
label="Action"
152+
value={this.triggerState}
153+
/>
154+
<limel-example-value
155+
label="Tag value"
156+
value={this.inputText}
157+
/>
158+
</div>
159+
</limel-example-controls>,
160+
this.renderPicker(),
161+
<limel-example-value value={this.value} />,
162+
];
163+
}
164+
165+
private handleTriggerStart = (event: CustomEvent<TriggerEventDetail>) => {
166+
this.triggerState = 'start';
167+
this.isPickerOpen = true;
168+
this.triggerFunction = event.detail.textEditor;
169+
};
170+
171+
private handleTriggerStop = () => {
172+
this.triggerState = 'stop';
173+
this.inputText = '';
174+
this.isPickerOpen = false;
175+
};
176+
177+
private handleTriggerChange = (event: CustomEvent<TriggerEventDetail>) => {
178+
this.inputText = event.detail.value.toLowerCase();
179+
};
180+
181+
private handleChange = (event: CustomEvent<string>) => {
182+
this.value = event.detail;
183+
};
184+
185+
private renderPicker = () => {
186+
if (!this.isPickerOpen) {
187+
return;
188+
}
189+
190+
const items = this.items.filter((item: ListItem<number>) =>
191+
item.text.toLowerCase().includes(this.inputText),
192+
);
193+
194+
const dropdownZIndex = getComputedStyle(this.host).getPropertyValue(
195+
'--dropdown-z-index',
196+
);
197+
198+
return [
199+
<limel-portal
200+
containerStyle={{
201+
'background-color': 'rgb(var(--contrast-100))',
202+
'border-radius': '0.5rem',
203+
'box-shadow': 'var(--shadow-depth-16)',
204+
'z-index': dropdownZIndex,
205+
}}
206+
containerId={this.portalId}
207+
visible={this.isPickerOpen}
208+
openDirection="bottom-start"
209+
inheritParentWidth={true}
210+
anchor={this.textEditorElement}
211+
>
212+
{this.renderList(items)}
213+
</limel-portal>,
214+
];
215+
};
216+
217+
private globalClickListener(event: MouseEvent) {
218+
const element: HTMLElement = event.target as HTMLElement;
219+
const clickedInside = portalContains(this.host, element);
220+
if (this.isPickerOpen && !clickedInside) {
221+
event.stopPropagation();
222+
event.preventDefault();
223+
this.isPickerOpen = false;
224+
this.close.emit();
225+
}
226+
}
227+
228+
private renderList = (items) => {
229+
if (items.length === 0) {
230+
return (
231+
<div style={{ padding: '0.5rem' }}>
232+
Couldn't find. Not a hero yet! 🥲
233+
</div>
234+
);
235+
}
236+
237+
return (
238+
<limel-list
239+
items={items}
240+
onChange={this.handleListChange}
241+
type="selectable"
242+
/>
243+
);
244+
};
245+
246+
private handleListChange = (event: CustomEvent<ListItem<number>>) => {
247+
if (this.insertMode === 'text') {
248+
this.triggerFunction.insert('@' + event.detail.text);
249+
250+
return;
251+
}
252+
253+
this.triggerFunction.insert({
254+
node: {
255+
tagName: 'limel-chip',
256+
attributes: {
257+
icon: event.detail.icon,
258+
text: event.detail.text,
259+
},
260+
},
261+
});
262+
};
263+
264+
private handleInsertModeChange = (event: CustomEvent<Button>) => {
265+
this.insertMode = event.detail.title as any;
266+
};
267+
}

0 commit comments

Comments
 (0)