Skip to content

Commit 16cc82a

Browse files
committed
docs(text-editor): Add example for custom trigger
1 parent 531a344 commit 16cc82a

File tree

3 files changed

+299
-1
lines changed

3 files changed

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

0 commit comments

Comments
 (0)