@devvit/ui-renderer 0.8.10 → 0.9.0
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/blocks/attributes.d.ts +30 -0
- package/blocks/attributes.d.ts.map +1 -0
- package/blocks/attributes.js +275 -0
- package/blocks/index.d.ts +3 -0
- package/blocks/index.d.ts.map +1 -0
- package/blocks/index.js +2 -0
- package/blocks/templates/index.d.ts +13 -0
- package/blocks/templates/index.d.ts.map +1 -0
- package/blocks/templates/index.js +37 -0
- package/blocks/templates/renderAnimationBlock.d.ts +5 -0
- package/blocks/templates/renderAnimationBlock.d.ts.map +1 -0
- package/blocks/templates/renderAnimationBlock.js +26 -0
- package/blocks/templates/renderAvatarBlock.d.ts +5 -0
- package/blocks/templates/renderAvatarBlock.d.ts.map +1 -0
- package/blocks/templates/renderAvatarBlock.js +26 -0
- package/blocks/templates/renderBlock.d.ts +5 -0
- package/blocks/templates/renderBlock.d.ts.map +1 -0
- package/blocks/templates/renderBlock.js +39 -0
- package/blocks/templates/renderButtonBlock.d.ts +5 -0
- package/blocks/templates/renderButtonBlock.d.ts.map +1 -0
- package/blocks/templates/renderButtonBlock.js +26 -0
- package/blocks/templates/renderFullSnooBlock.d.ts +5 -0
- package/blocks/templates/renderFullSnooBlock.d.ts.map +1 -0
- package/blocks/templates/renderFullSnooBlock.js +26 -0
- package/blocks/templates/renderIconBlock.d.ts +5 -0
- package/blocks/templates/renderIconBlock.d.ts.map +1 -0
- package/blocks/templates/renderIconBlock.js +17 -0
- package/blocks/templates/renderImageBlock.d.ts +5 -0
- package/blocks/templates/renderImageBlock.d.ts.map +1 -0
- package/blocks/templates/renderImageBlock.js +31 -0
- package/blocks/templates/renderSpacerBlock.d.ts +5 -0
- package/blocks/templates/renderSpacerBlock.d.ts.map +1 -0
- package/blocks/templates/renderSpacerBlock.js +14 -0
- package/blocks/templates/renderStackBlock.d.ts +5 -0
- package/blocks/templates/renderStackBlock.d.ts.map +1 -0
- package/blocks/templates/renderStackBlock.js +38 -0
- package/blocks/templates/renderTextBlock.d.ts +5 -0
- package/blocks/templates/renderTextBlock.d.ts.map +1 -0
- package/blocks/templates/renderTextBlock.js +59 -0
- package/blocks/templates/renderWebViewBlock.d.ts +5 -0
- package/blocks/templates/renderWebViewBlock.d.ts.map +1 -0
- package/blocks/templates/renderWebViewBlock.js +36 -0
- package/client/assets/snooBlocks.d.ts +2 -0
- package/client/assets/snooBlocks.d.ts.map +1 -0
- package/client/assets/snooBlocks.js +2 -0
- package/client/{renderer.d.ts → blocks.d.ts} +2 -2
- package/client/blocks.d.ts.map +1 -0
- package/client/{renderer.js → blocks.js} +1 -1
- package/client/devvit-animation-player.d.ts +2 -2
- package/client/devvit-animation-player.d.ts.map +1 -1
- package/client/devvit-animation-player.js +3 -4
- package/client/devvit-custom-post.d.ts +25 -0
- package/client/devvit-custom-post.d.ts.map +1 -0
- package/client/devvit-custom-post.js +147 -0
- package/client/effects/devvit-effect-handler.d.ts +18 -0
- package/client/effects/devvit-effect-handler.d.ts.map +1 -0
- package/client/effects/devvit-effect-handler.js +58 -0
- package/client/effects/form-effect-handler.d.ts +9 -0
- package/client/effects/form-effect-handler.d.ts.map +1 -0
- package/client/effects/form-effect-handler.js +15 -0
- package/client/effects/toast-effect-handler.d.ts +6 -0
- package/client/effects/toast-effect-handler.d.ts.map +1 -0
- package/client/effects/toast-effect-handler.js +12 -0
- package/package.json +11 -10
- package/server/{renderer.d.ts → blocks.d.ts} +2 -2
- package/server/blocks.d.ts.map +1 -0
- package/server/{renderer.js → blocks.js} +1 -1
- package/styles.css +42 -1
- package/tailwind.devvit.cjs +15 -0
- package/client/devvit-ui-interactive.d.ts +0 -34
- package/client/devvit-ui-interactive.d.ts.map +0 -1
- package/client/devvit-ui-interactive.js +0 -229
- package/client/renderer.d.ts.map +0 -1
- package/render-core.d.ts +0 -20
- package/render-core.d.ts.map +0 -1
- package/render-core.js +0 -347
- package/server/renderer.d.ts.map +0 -1
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component that renders wraps a rendering of devvit ui and makes it interactive.
|
|
3
|
-
*
|
|
4
|
-
* Needs to handle
|
|
5
|
-
* 1 - not getting slotted content and IT triggers the first render
|
|
6
|
-
* 2 - getting slotted content from SSR and then replacing it when interactivity happens
|
|
7
|
-
*/
|
|
8
|
-
/// <reference lib="dom" />
|
|
9
|
-
var _DevvitInteractiveUI_instances, _DevvitInteractiveUI_customPost, _DevvitInteractiveUI_uiEventHandler, _DevvitInteractiveUI_context, _DevvitInteractiveUI_rerenderTimeout, _DevvitInteractiveUI_realtimeSubscription, _DevvitInteractiveUI_onMessage, _DevvitInteractiveUI_triggerFirstRender, _DevvitInteractiveUI_requestRender, _DevvitInteractiveUI_onClick, _DevvitInteractiveUI_handleEffects, _DevvitInteractiveUI_handleEvents, _DevvitInteractiveUI_rerenderEffect, _DevvitInteractiveUI_realtimeSubscriptionsEffect, _DevvitInteractiveUI_showToastEffect;
|
|
10
|
-
import { __classPrivateFieldGet, __classPrivateFieldSet, __decorate, __metadata } from "tslib";
|
|
11
|
-
import { css, html, LitElement } from 'lit';
|
|
12
|
-
import { customElement, property, state } from 'lit/decorators.js';
|
|
13
|
-
import { BlocksEvent, BlocksEventType, CustomPostDefinition, RenderPostResponse, ToastAppearance, UIEventHandlerDefinition, } from '@devvit/protos';
|
|
14
|
-
import tailwind from '@reddit/shreddit.styles';
|
|
15
|
-
import { customEvent } from '@reddit/faceplate/lib/custom-event.js';
|
|
16
|
-
import { Severity } from '@reddit/faceplate/types.js';
|
|
17
|
-
import { renderRoot } from './renderer.js';
|
|
18
|
-
import './devvit-animation-player.js';
|
|
19
|
-
let DevvitInteractiveUI = class DevvitInteractiveUI extends LitElement {
|
|
20
|
-
constructor() {
|
|
21
|
-
super(...arguments);
|
|
22
|
-
_DevvitInteractiveUI_instances.add(this);
|
|
23
|
-
_DevvitInteractiveUI_customPost.set(this, void 0);
|
|
24
|
-
_DevvitInteractiveUI_uiEventHandler.set(this, void 0);
|
|
25
|
-
_DevvitInteractiveUI_context.set(this, {});
|
|
26
|
-
_DevvitInteractiveUI_rerenderTimeout.set(this, void 0);
|
|
27
|
-
_DevvitInteractiveUI_realtimeSubscription.set(this, void 0);
|
|
28
|
-
_DevvitInteractiveUI_onMessage.set(this, (event) => {
|
|
29
|
-
console.log('got message', event);
|
|
30
|
-
const evt = BlocksEvent.fromJSON(event.data);
|
|
31
|
-
if (evt.type === BlocksEventType.USER_ACTION) {
|
|
32
|
-
const req = {
|
|
33
|
-
context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
34
|
-
blocks: {
|
|
35
|
-
event: evt,
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
_DevvitInteractiveUI_handleEvents.set(this, async (event) => {
|
|
42
|
-
const res = await __classPrivateFieldGet(this, _DevvitInteractiveUI_uiEventHandler, "f")?.HandleUIEvent({
|
|
43
|
-
context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
44
|
-
event,
|
|
45
|
-
});
|
|
46
|
-
if (res) {
|
|
47
|
-
__classPrivateFieldSet(this, _DevvitInteractiveUI_context, res.context, "f");
|
|
48
|
-
await __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_handleEffects).call(this, res.effects);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
static get styles() {
|
|
53
|
-
return [
|
|
54
|
-
tailwind,
|
|
55
|
-
// dynamic mode styles only. 99% of things should come statically from tailwind
|
|
56
|
-
css ``,
|
|
57
|
-
];
|
|
58
|
-
}
|
|
59
|
-
connectedCallback() {
|
|
60
|
-
super.connectedCallback();
|
|
61
|
-
window.addEventListener('message', __classPrivateFieldGet(this, _DevvitInteractiveUI_onMessage, "f"));
|
|
62
|
-
__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_triggerFirstRender).call(this);
|
|
63
|
-
}
|
|
64
|
-
disconnectedCallback() {
|
|
65
|
-
super.disconnectedCallback();
|
|
66
|
-
console.log('disconnected from custom post preview');
|
|
67
|
-
window.removeEventListener('message', __classPrivateFieldGet(this, _DevvitInteractiveUI_onMessage, "f"));
|
|
68
|
-
}
|
|
69
|
-
willUpdate(changedProperties) {
|
|
70
|
-
if (changedProperties.has('actorRef')) {
|
|
71
|
-
__classPrivateFieldSet(this, _DevvitInteractiveUI_customPost, this.actorRef?.As(CustomPostDefinition), "f");
|
|
72
|
-
__classPrivateFieldSet(this, _DevvitInteractiveUI_uiEventHandler, this.actorRef?.As(UIEventHandlerDefinition), "f");
|
|
73
|
-
__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_triggerFirstRender).call(this);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
render() {
|
|
77
|
-
// At first, we don't have a render response. In the meantime allow
|
|
78
|
-
// slot'd content to be rendered. This lets SSR work.
|
|
79
|
-
if (!this.renderResponse) {
|
|
80
|
-
return html `<slot></slot>`;
|
|
81
|
-
}
|
|
82
|
-
const uiRoot = this.renderResponse.blocks?.ui;
|
|
83
|
-
// TODO better handle case where there is no UI to render
|
|
84
|
-
if (!uiRoot) {
|
|
85
|
-
// eslint-disable-next-line @reddit/i18n-shreddit/no-unwrapped-strings
|
|
86
|
-
return html `<div>No UI to render</div>`;
|
|
87
|
-
}
|
|
88
|
-
return html `<div @click="${__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_onClick)}">${renderRoot(uiRoot, this.renderResponse)}</div>`;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
_DevvitInteractiveUI_customPost = new WeakMap(), _DevvitInteractiveUI_uiEventHandler = new WeakMap(), _DevvitInteractiveUI_context = new WeakMap(), _DevvitInteractiveUI_rerenderTimeout = new WeakMap(), _DevvitInteractiveUI_realtimeSubscription = new WeakMap(), _DevvitInteractiveUI_onMessage = new WeakMap(), _DevvitInteractiveUI_handleEvents = new WeakMap(), _DevvitInteractiveUI_instances = new WeakSet(), _DevvitInteractiveUI_triggerFirstRender = function _DevvitInteractiveUI_triggerFirstRender() {
|
|
92
|
-
if (!this.actorRef || !__classPrivateFieldGet(this, _DevvitInteractiveUI_customPost, "f")) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const req = {
|
|
96
|
-
blocks: {
|
|
97
|
-
event: {
|
|
98
|
-
type: BlocksEventType.INITIAL_RENDER,
|
|
99
|
-
key: 'initial-render',
|
|
100
|
-
data: {},
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
context: {},
|
|
104
|
-
};
|
|
105
|
-
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
106
|
-
}, _DevvitInteractiveUI_requestRender =
|
|
107
|
-
// TODO block on only one render request going through at a time
|
|
108
|
-
async function _DevvitInteractiveUI_requestRender(req) {
|
|
109
|
-
try {
|
|
110
|
-
const response = await __classPrivateFieldGet(this, _DevvitInteractiveUI_customPost, "f")?.RenderPost(req, this.metadata);
|
|
111
|
-
if (response) {
|
|
112
|
-
__classPrivateFieldSet(this, _DevvitInteractiveUI_context, response.context, "f");
|
|
113
|
-
if (response.blocks) {
|
|
114
|
-
this.renderResponse = response;
|
|
115
|
-
this.onRender?.(response);
|
|
116
|
-
}
|
|
117
|
-
if (response.effects) {
|
|
118
|
-
await __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_handleEffects).call(this, response.effects);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
console.warn('No response from custom post');
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
console.error('Error while running custom post', err);
|
|
127
|
-
}
|
|
128
|
-
}, _DevvitInteractiveUI_onClick = function _DevvitInteractiveUI_onClick(event) {
|
|
129
|
-
// walk the tree and find the nearest data-action-id
|
|
130
|
-
// and use that to send a new render request into the app
|
|
131
|
-
let target = event.target;
|
|
132
|
-
let actionId;
|
|
133
|
-
while (target) {
|
|
134
|
-
actionId = target.dataset.actionId;
|
|
135
|
-
if (actionId) {
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
target = target.parentElement || undefined;
|
|
139
|
-
}
|
|
140
|
-
if (actionId) {
|
|
141
|
-
const req = {
|
|
142
|
-
context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
143
|
-
blocks: {
|
|
144
|
-
event: {
|
|
145
|
-
type: BlocksEventType.USER_ACTION,
|
|
146
|
-
key: actionId,
|
|
147
|
-
data: {},
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
152
|
-
}
|
|
153
|
-
}, _DevvitInteractiveUI_handleEffects = async function _DevvitInteractiveUI_handleEffects(effects) {
|
|
154
|
-
const running = [];
|
|
155
|
-
for (const effect of effects) {
|
|
156
|
-
if (effect.rerenderUi) {
|
|
157
|
-
running.push(__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_rerenderEffect).call(this, effect.rerenderUi));
|
|
158
|
-
}
|
|
159
|
-
else if (effect.realtimeSubscriptions) {
|
|
160
|
-
__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_realtimeSubscriptionsEffect).call(this, effect.realtimeSubscriptions);
|
|
161
|
-
}
|
|
162
|
-
else if (effect.showToast) {
|
|
163
|
-
__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_showToastEffect).call(this, effect.showToast);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
// Can't handle this directly. Bubble effect up.
|
|
167
|
-
this.dispatchEvent(customEvent('devvit-ui-effect', { effect, onEvent: __classPrivateFieldGet(this, _DevvitInteractiveUI_handleEvents, "f") }));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
await Promise.allSettled(running);
|
|
171
|
-
}, _DevvitInteractiveUI_rerenderEffect = async function _DevvitInteractiveUI_rerenderEffect(effect) {
|
|
172
|
-
const rerender = async () => {
|
|
173
|
-
__classPrivateFieldSet(this, _DevvitInteractiveUI_rerenderTimeout, undefined, "f");
|
|
174
|
-
await __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, {
|
|
175
|
-
context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
176
|
-
blocks: {
|
|
177
|
-
event: {
|
|
178
|
-
type: BlocksEventType.EFFECT_ACTION,
|
|
179
|
-
key: 'rerender-effect',
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
};
|
|
184
|
-
// Rerender already queued. Resolve it early and queue another.
|
|
185
|
-
if (__classPrivateFieldGet(this, _DevvitInteractiveUI_rerenderTimeout, "f")) {
|
|
186
|
-
window.clearTimeout(__classPrivateFieldGet(this, _DevvitInteractiveUI_rerenderTimeout, "f"));
|
|
187
|
-
await rerender();
|
|
188
|
-
}
|
|
189
|
-
__classPrivateFieldSet(this, _DevvitInteractiveUI_rerenderTimeout, window.setTimeout(() => void rerender(), (effect.delaySeconds ?? 0) * 1000), "f");
|
|
190
|
-
}, _DevvitInteractiveUI_realtimeSubscriptionsEffect = function _DevvitInteractiveUI_realtimeSubscriptionsEffect(effect) {
|
|
191
|
-
__classPrivateFieldSet(this, _DevvitInteractiveUI_realtimeSubscription, this.realtime
|
|
192
|
-
?.Subscribe({ channels: effect.subscriptionIds }, this.metadata)
|
|
193
|
-
.subscribe((event) => {
|
|
194
|
-
void __classPrivateFieldGet(this, _DevvitInteractiveUI_handleEvents, "f").call(this, { realtimeEvent: { event } });
|
|
195
|
-
}), "f");
|
|
196
|
-
}, _DevvitInteractiveUI_showToastEffect = function _DevvitInteractiveUI_showToastEffect(effect) {
|
|
197
|
-
// TODO:
|
|
198
|
-
// - effect.toast.leadingElement
|
|
199
|
-
// - effect.toast.trailingElement
|
|
200
|
-
// - onAction
|
|
201
|
-
this.dispatchEvent(customEvent('faceplate-alert', {
|
|
202
|
-
level: effect.toast?.appearance === ToastAppearance.SUCCESS ? Severity.success : Severity.info,
|
|
203
|
-
message: effect.toast?.text,
|
|
204
|
-
}));
|
|
205
|
-
};
|
|
206
|
-
__decorate([
|
|
207
|
-
property({ attribute: false }),
|
|
208
|
-
__metadata("design:type", Object)
|
|
209
|
-
], DevvitInteractiveUI.prototype, "actorRef", void 0);
|
|
210
|
-
__decorate([
|
|
211
|
-
property({ attribute: false }),
|
|
212
|
-
__metadata("design:type", Object)
|
|
213
|
-
], DevvitInteractiveUI.prototype, "realtime", void 0);
|
|
214
|
-
__decorate([
|
|
215
|
-
property({ attribute: false }),
|
|
216
|
-
__metadata("design:type", Object)
|
|
217
|
-
], DevvitInteractiveUI.prototype, "metadata", void 0);
|
|
218
|
-
__decorate([
|
|
219
|
-
property({ attribute: false }),
|
|
220
|
-
__metadata("design:type", Function)
|
|
221
|
-
], DevvitInteractiveUI.prototype, "onRender", void 0);
|
|
222
|
-
__decorate([
|
|
223
|
-
state(),
|
|
224
|
-
__metadata("design:type", Object)
|
|
225
|
-
], DevvitInteractiveUI.prototype, "renderResponse", void 0);
|
|
226
|
-
DevvitInteractiveUI = __decorate([
|
|
227
|
-
customElement('devvit-ui-interactive')
|
|
228
|
-
], DevvitInteractiveUI);
|
|
229
|
-
export { DevvitInteractiveUI };
|
package/client/renderer.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/client/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,kEAAkE,CAAC;AAC1E,cAAc,mBAAmB,CAAC"}
|
package/render-core.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core rendering functions for devvit UI elements.
|
|
3
|
-
*
|
|
4
|
-
* This uses @reddit/faceplate-ui's renderStrategies to render the UI. This is necessary
|
|
5
|
-
* because we need to render the UI on the server-side shreddit style, and also client-side.
|
|
6
|
-
* Examples of where this will be used:
|
|
7
|
-
* - dev-server's preview components
|
|
8
|
-
* - shreddit
|
|
9
|
-
* - d2x
|
|
10
|
-
* - other reddit clients
|
|
11
|
-
*
|
|
12
|
-
* All styling will be done with tailwind classes whenever possible.
|
|
13
|
-
*
|
|
14
|
-
* Styling not feasible with tailwind classes will be done with inline styles or css classes
|
|
15
|
-
*/
|
|
16
|
-
import { Element, RenderPostResponse } from '@devvit/protos';
|
|
17
|
-
import type { TemplateLike } from '@reddit/baseplate/html.js';
|
|
18
|
-
export declare const renderRoot: (element: Element, rsp: RenderPostResponse) => TemplateLike;
|
|
19
|
-
export declare const renderElement: (element: Element, rsp: RenderPostResponse) => TemplateLike;
|
|
20
|
-
//# sourceMappingURL=render-core.d.ts.map
|
package/render-core.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"render-core.d.ts","sourceRoot":"","sources":["../src/render-core.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAEL,OAAO,EAOP,kBAAkB,EAKnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAc9D,eAAO,MAAM,UAAU,YAAa,OAAO,OAAO,kBAAkB,KAAG,YAKtE,CAAC;AAEF,eAAO,MAAM,aAAa,YAAa,OAAO,OAAO,kBAAkB,KAAG,YAsBzE,CAAC"}
|
package/render-core.js
DELETED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @reddit/i18n-shreddit/no-unwrapped-strings */
|
|
2
|
-
/**
|
|
3
|
-
* Core rendering functions for devvit UI elements.
|
|
4
|
-
*
|
|
5
|
-
* This uses @reddit/faceplate-ui's renderStrategies to render the UI. This is necessary
|
|
6
|
-
* because we need to render the UI on the server-side shreddit style, and also client-side.
|
|
7
|
-
* Examples of where this will be used:
|
|
8
|
-
* - dev-server's preview components
|
|
9
|
-
* - shreddit
|
|
10
|
-
* - d2x
|
|
11
|
-
* - other reddit clients
|
|
12
|
-
*
|
|
13
|
-
* All styling will be done with tailwind classes whenever possible.
|
|
14
|
-
*
|
|
15
|
-
* Styling not feasible with tailwind classes will be done with inline styles or css classes
|
|
16
|
-
*/
|
|
17
|
-
import { AnimationFormat, ElementType, ButtonAppearance, ButtonShape, ButtonSize, ObjectFit, Padding, StackAlign, StackDirection, StackRoundedCornerSize, TextAlign, } from '@devvit/protos';
|
|
18
|
-
import { getTemplateRenderingStrategy } from '@reddit/faceplate-ui/faceplateUIConfig.js';
|
|
19
|
-
// Faceplate Client|Server agnostic templates.
|
|
20
|
-
// These cannot be called reliably until the faceplate rendering strategy is set.
|
|
21
|
-
// To make sure that works, callers should import the render functions from the client/server
|
|
22
|
-
// directories instead of this file.
|
|
23
|
-
import { button as faceplateButton } from '@reddit/faceplate-ui/templates/button.js';
|
|
24
|
-
import { ButtonSize as FPButtonSize } from '@reddit/faceplate-ui/templates/button.js';
|
|
25
|
-
// Rendering functions
|
|
26
|
-
export const renderRoot = (element, rsp) => {
|
|
27
|
-
const { html } = getTemplateRenderingStrategy();
|
|
28
|
-
return html `<div class="font-semibold p-sm text-12 font-sans tracking-tight leading-5">
|
|
29
|
-
${renderElement(element, rsp)}
|
|
30
|
-
</div>`;
|
|
31
|
-
};
|
|
32
|
-
export const renderElement = (element, rsp) => {
|
|
33
|
-
switch (element.type) {
|
|
34
|
-
case ElementType.STACK:
|
|
35
|
-
return renderStack(element, rsp);
|
|
36
|
-
case ElementType.IMAGE:
|
|
37
|
-
return renderImage(element);
|
|
38
|
-
case ElementType.ANIMATION:
|
|
39
|
-
return renderAnimation(element);
|
|
40
|
-
case ElementType.BUTTON:
|
|
41
|
-
return renderButton(element);
|
|
42
|
-
case ElementType.SPACER:
|
|
43
|
-
return renderSpacer(element);
|
|
44
|
-
case ElementType.FRAGMENT:
|
|
45
|
-
return renderFragment(element, rsp);
|
|
46
|
-
case ElementType.TEXT:
|
|
47
|
-
return renderText(element);
|
|
48
|
-
case ElementType.WEB_VIEW:
|
|
49
|
-
return renderWebView(element, rsp);
|
|
50
|
-
}
|
|
51
|
-
const { html } = getTemplateRenderingStrategy();
|
|
52
|
-
return html `DEBUG ME, unknown element type: ${element.type}`;
|
|
53
|
-
};
|
|
54
|
-
const refBySrc = {};
|
|
55
|
-
const renderWebView = (element, rsp) => {
|
|
56
|
-
const { html, styleMap } = getTemplateRenderingStrategy();
|
|
57
|
-
console.log('rendering webview', element, rsp);
|
|
58
|
-
postMessageToIframe(element.src, rsp);
|
|
59
|
-
return html `<iframe
|
|
60
|
-
class="border-box ${grow(element)}"
|
|
61
|
-
@load="${(evt) => {
|
|
62
|
-
refBySrc[element.src] = evt.target;
|
|
63
|
-
postMessageToIframe(element.src, rsp);
|
|
64
|
-
}}"
|
|
65
|
-
ref="${(el) => (refBySrc[element.src] = el)}"
|
|
66
|
-
sandbox="allow-scripts"
|
|
67
|
-
src="${element.src}"
|
|
68
|
-
style="${styleMap({
|
|
69
|
-
backgroundColor: element.backgroundColor,
|
|
70
|
-
borderColor: element.borderColor,
|
|
71
|
-
borderWidth: element.borderColor ? '1px' : '0',
|
|
72
|
-
borderRadius: element.roundedCornerSize ? `${element.roundedCornerSize}px` : undefined,
|
|
73
|
-
})}"
|
|
74
|
-
></iframe>`;
|
|
75
|
-
};
|
|
76
|
-
const postMessageToIframe = (src, rsp) => {
|
|
77
|
-
const iframe = refBySrc[src];
|
|
78
|
-
console.log('posting message to iframe', iframe, rsp);
|
|
79
|
-
iframe?.contentWindow?.postMessage(rsp, '*');
|
|
80
|
-
};
|
|
81
|
-
const renderStack = (element, rsp) => {
|
|
82
|
-
const layoutClass = getStackLayoutClass(element);
|
|
83
|
-
const flexDirection = getFlexDirectionClass(element);
|
|
84
|
-
const justifyContent = justifyContentClass(element);
|
|
85
|
-
const alignContent = alignContentClass(element.crossAlign);
|
|
86
|
-
const borderRadiusClass = getBorderRadiusClass(element.roundedCornerSize);
|
|
87
|
-
const paddingClass = getPaddingClass(element);
|
|
88
|
-
const style = {
|
|
89
|
-
backgroundColor: element.backgroundColor,
|
|
90
|
-
border: element.borderColor && `1px ${element.borderColor} solid`,
|
|
91
|
-
};
|
|
92
|
-
if (element.sizePercent) {
|
|
93
|
-
if (element.stackDirection === StackDirection.VERTICAL) {
|
|
94
|
-
style.height = `${element.sizePercent}%`;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
style.width = `${element.sizePercent}%`;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const { html, repeat, styleMap } = getTemplateRenderingStrategy();
|
|
101
|
-
return html `<div
|
|
102
|
-
class="border-box ${layoutClass} ${grow(element)} ${flexDirection} ${alignContent} ${justifyContent} ${borderRadiusClass} ${paddingClass}"
|
|
103
|
-
style="${styleMap(style)}"
|
|
104
|
-
data-action-id="${element.actionId}"
|
|
105
|
-
>
|
|
106
|
-
${repeat(element.children, (e) => renderElement(e, rsp))}
|
|
107
|
-
</div>`;
|
|
108
|
-
};
|
|
109
|
-
const grow = (element) => {
|
|
110
|
-
return element.grow ? 'grow' : '';
|
|
111
|
-
};
|
|
112
|
-
const getPaddingClass = (element) => {
|
|
113
|
-
if (element.stackDirection === StackDirection.DEPTH) {
|
|
114
|
-
return '';
|
|
115
|
-
}
|
|
116
|
-
if (element.padding === Padding.PADDING_LARGE) {
|
|
117
|
-
return 'p-lg';
|
|
118
|
-
}
|
|
119
|
-
else if (element.padding === Padding.PADDING_MEDIUM) {
|
|
120
|
-
return 'p-md';
|
|
121
|
-
}
|
|
122
|
-
else if (element.padding === Padding.PADDING_SMALL) {
|
|
123
|
-
return 'p-sm';
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
return '';
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
const getFlexDirectionClass = (element) => {
|
|
130
|
-
let flexDirection = 'flex-row';
|
|
131
|
-
if (element.stackDirection === StackDirection.VERTICAL) {
|
|
132
|
-
if (element.stackReverse) {
|
|
133
|
-
flexDirection = 'flex-col-reverse';
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
flexDirection = 'flex-col';
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
else if (element.stackDirection === StackDirection.DEPTH) {
|
|
140
|
-
flexDirection = 'devvit-depth';
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
if (element.stackReverse) {
|
|
144
|
-
flexDirection = 'flex-row-reverse';
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return flexDirection;
|
|
148
|
-
};
|
|
149
|
-
const getStackLayoutClass = (element) => {
|
|
150
|
-
if (element.stackDirection === StackDirection.DEPTH) {
|
|
151
|
-
return 'devvit-depth';
|
|
152
|
-
}
|
|
153
|
-
return 'flex';
|
|
154
|
-
};
|
|
155
|
-
const justifyContentClass = (element) => {
|
|
156
|
-
const zStack = element.stackDirection === StackDirection.DEPTH;
|
|
157
|
-
switch (element.stackAlign) {
|
|
158
|
-
case StackAlign.STACK_START:
|
|
159
|
-
return zStack ? 'justify-items-start' : 'justify-start';
|
|
160
|
-
case StackAlign.STACK_END:
|
|
161
|
-
return zStack ? 'justify-items-end' : `justify-end`;
|
|
162
|
-
case StackAlign.STACK_CENTER:
|
|
163
|
-
return zStack ? 'justify-items-center' : `justify-center`;
|
|
164
|
-
case undefined:
|
|
165
|
-
default:
|
|
166
|
-
return zStack ? '' : 'justify-center';
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const alignContentClass = (align) => {
|
|
170
|
-
switch (align) {
|
|
171
|
-
case StackAlign.STACK_START:
|
|
172
|
-
return 'items-start';
|
|
173
|
-
case StackAlign.STACK_END:
|
|
174
|
-
return 'items-end';
|
|
175
|
-
case StackAlign.STACK_CENTER:
|
|
176
|
-
return 'items-center';
|
|
177
|
-
case undefined:
|
|
178
|
-
default:
|
|
179
|
-
return 'items-stretch';
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
const getBorderRadiusClass = (rounding) => {
|
|
183
|
-
switch (rounding) {
|
|
184
|
-
case StackRoundedCornerSize.CORNER_NONE:
|
|
185
|
-
return 'rounded-none';
|
|
186
|
-
case StackRoundedCornerSize.CORNER_SMALL:
|
|
187
|
-
return 'rounded-sm';
|
|
188
|
-
case StackRoundedCornerSize.CORNER_MEDIUM:
|
|
189
|
-
// TODO this might not work in faceplate's tailwind config
|
|
190
|
-
// see https://faceplate-ui.snooguts.net/tailwind/#Border%20Radius
|
|
191
|
-
return 'rounded-md';
|
|
192
|
-
case StackRoundedCornerSize.CORNER_LARGE:
|
|
193
|
-
return 'rounded-lg';
|
|
194
|
-
case StackRoundedCornerSize.CORNER_FULL:
|
|
195
|
-
return 'rounded-full';
|
|
196
|
-
default:
|
|
197
|
-
return 'rounded-none';
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
const renderImage = (element) => {
|
|
201
|
-
const { html } = getTemplateRenderingStrategy();
|
|
202
|
-
// note: empty ' ' at start of `class` to avoid false-positive in
|
|
203
|
-
// lit/quoted-expressions rule
|
|
204
|
-
return html `<img
|
|
205
|
-
class=" ${getObjectFitClass(element.objectFit)} ${grow(element)}"
|
|
206
|
-
referrerpolicy="no-referrer"
|
|
207
|
-
crossorigin="anonymous"
|
|
208
|
-
loading="lazy"
|
|
209
|
-
src="${element.src}"
|
|
210
|
-
data-action-id="${element.actionId}"
|
|
211
|
-
/>`;
|
|
212
|
-
};
|
|
213
|
-
const renderAnimation = (element) => {
|
|
214
|
-
const { html } = getTemplateRenderingStrategy();
|
|
215
|
-
const { src, animationFormat } = element;
|
|
216
|
-
return html `
|
|
217
|
-
<devvit-animation-player
|
|
218
|
-
class=" ${getObjectFitClass(element.objectFit)} ${grow(element)}"
|
|
219
|
-
src="${src}"
|
|
220
|
-
format="${animationFormat ?? AnimationFormat.LOTTIE}"
|
|
221
|
-
>
|
|
222
|
-
</devvit-animation-player>
|
|
223
|
-
`;
|
|
224
|
-
};
|
|
225
|
-
const getObjectFitClass = (fit) => {
|
|
226
|
-
switch (fit) {
|
|
227
|
-
case ObjectFit.CONTAIN:
|
|
228
|
-
return 'object-contain';
|
|
229
|
-
case ObjectFit.COVER:
|
|
230
|
-
return 'object-cover';
|
|
231
|
-
case ObjectFit.FILL:
|
|
232
|
-
return 'object-fill';
|
|
233
|
-
case ObjectFit.SCALE_DOWN:
|
|
234
|
-
return 'object-scale-down';
|
|
235
|
-
case ObjectFit.NONE:
|
|
236
|
-
return 'object-none';
|
|
237
|
-
}
|
|
238
|
-
return '';
|
|
239
|
-
};
|
|
240
|
-
const renderButton = (element) => {
|
|
241
|
-
// TODO improve button rendering
|
|
242
|
-
// TODO figure out how to dynamically pick the client vs server faceplate template
|
|
243
|
-
const { html } = getTemplateRenderingStrategy();
|
|
244
|
-
return faceplateButton({
|
|
245
|
-
attributes: {
|
|
246
|
-
className: grow(element),
|
|
247
|
-
'data-action-id': element.actionId,
|
|
248
|
-
},
|
|
249
|
-
children: html `${element.text}`,
|
|
250
|
-
shape: getFPButtonShape(element),
|
|
251
|
-
appearance: getFPButtonAppearance(element),
|
|
252
|
-
size: getFPButtonSize(element),
|
|
253
|
-
});
|
|
254
|
-
};
|
|
255
|
-
const getFPButtonShape = (element) => {
|
|
256
|
-
switch (element.buttonShape) {
|
|
257
|
-
case ButtonShape.SQUARE:
|
|
258
|
-
return 'square';
|
|
259
|
-
case ButtonShape.PILL:
|
|
260
|
-
case ButtonShape.UNRECOGNIZED:
|
|
261
|
-
default:
|
|
262
|
-
return 'pill';
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
const getFPButtonAppearance = (element) => {
|
|
266
|
-
switch (element.buttonAppearance) {
|
|
267
|
-
case ButtonAppearance.BUTTON_PRIMARY:
|
|
268
|
-
return 'primary';
|
|
269
|
-
case ButtonAppearance.BUTTON_PLAIN:
|
|
270
|
-
return 'plain';
|
|
271
|
-
case ButtonAppearance.BUTTON_OUTLINE:
|
|
272
|
-
return 'outline';
|
|
273
|
-
case ButtonAppearance.BUTTON_DESTRUCTIVE:
|
|
274
|
-
return 'destructive';
|
|
275
|
-
case ButtonAppearance.BUTTON_MEDIA:
|
|
276
|
-
return 'media';
|
|
277
|
-
case ButtonAppearance.BUTTON_BRAND:
|
|
278
|
-
return 'brand';
|
|
279
|
-
case ButtonAppearance.BUTTON_SUCCESS:
|
|
280
|
-
return 'success';
|
|
281
|
-
case ButtonAppearance.BUTTON_SECONDARY:
|
|
282
|
-
default:
|
|
283
|
-
return 'secondary';
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
const getFPButtonSize = (element) => {
|
|
287
|
-
switch (element.buttonSize) {
|
|
288
|
-
case ButtonSize.BUTTON_SMALL:
|
|
289
|
-
return FPButtonSize.Small;
|
|
290
|
-
case ButtonSize.BUTTON_LARGE:
|
|
291
|
-
return FPButtonSize.Large;
|
|
292
|
-
case ButtonSize.BUTTON_MEDIUM:
|
|
293
|
-
default:
|
|
294
|
-
return FPButtonSize.Medium;
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
const getSpacerPaddingClass = (element) => {
|
|
298
|
-
if (element.padding === Padding.PADDING_LARGE) {
|
|
299
|
-
return 'pr-lg pb-lg';
|
|
300
|
-
}
|
|
301
|
-
else if (element.padding === Padding.PADDING_MEDIUM) {
|
|
302
|
-
return 'pr-md pb-md';
|
|
303
|
-
}
|
|
304
|
-
else if (element.padding === Padding.PADDING_SMALL) {
|
|
305
|
-
return 'pr-sm pb-sm';
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
return '';
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
const renderSpacer = (element) => {
|
|
312
|
-
const paddingClass = getSpacerPaddingClass(element);
|
|
313
|
-
const { html } = getTemplateRenderingStrategy();
|
|
314
|
-
// note: empty ' ' at start of `class` to avoid false-positive in
|
|
315
|
-
// lit/quoted-expressions rule
|
|
316
|
-
return html `<div class=" ${grow(element)} ${paddingClass}"></div>`;
|
|
317
|
-
};
|
|
318
|
-
const renderFragment = (element, rsp) => {
|
|
319
|
-
const { html, repeat } = getTemplateRenderingStrategy();
|
|
320
|
-
return html `${repeat(element.children, (e) => renderElement(e, rsp))}`;
|
|
321
|
-
};
|
|
322
|
-
const renderText = (element) => {
|
|
323
|
-
const style = {
|
|
324
|
-
fontSize: element.fontSize && element.fontSize !== 0 ? `${element.fontSize}em` : undefined,
|
|
325
|
-
color: element.color,
|
|
326
|
-
width: element.sizePercent ? `${element.sizePercent}%` : undefined,
|
|
327
|
-
};
|
|
328
|
-
const textAlignClass = getTextAlignClass(element.textAlign);
|
|
329
|
-
const { html, styleMap } = getTemplateRenderingStrategy();
|
|
330
|
-
// note: empty ' ' at start of `class` to avoid false-positive in
|
|
331
|
-
// lit/quoted-expressions rule
|
|
332
|
-
return html `<div class=" ${textAlignClass} ${grow(element)}" style="${styleMap(style)}">
|
|
333
|
-
${element.text}
|
|
334
|
-
</div>`;
|
|
335
|
-
};
|
|
336
|
-
const getTextAlignClass = (align) => {
|
|
337
|
-
switch (align) {
|
|
338
|
-
case TextAlign.TEXT_ALIGN_CENTER:
|
|
339
|
-
return 'text-center';
|
|
340
|
-
case TextAlign.TEXT_ALIGN_END:
|
|
341
|
-
return 'text-end';
|
|
342
|
-
case TextAlign.TEXT_ALIGN_START:
|
|
343
|
-
case undefined:
|
|
344
|
-
default:
|
|
345
|
-
return 'text-start';
|
|
346
|
-
}
|
|
347
|
-
};
|
package/server/renderer.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/server/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,kEAAkE,CAAC;AAC1E,cAAc,mBAAmB,CAAC"}
|