Skip to content
This repository was archived by the owner on Nov 22, 2022. It is now read-only.

Commit 71cb89d

Browse files
Add canvas.Frame component
1 parent 9fcd281 commit 71cb89d

File tree

4 files changed

+177
-3
lines changed

4 files changed

+177
-3
lines changed

packages/library/src/canvas.js

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Canvas-based displays for lab.js
22
import { Component } from './core'
3-
import { Sequence as BaseSequence } from './flow'
3+
import { Sequence as BaseSequence, Loop, Parallel,
4+
prepareNested } from './flow'
5+
import { Frame as BaseFrame } from './html'
6+
import { reduce } from './util/tree'
47

58
// Global canvas functions used in all of the following components
69
// (multiple inheritance would come in handy here, but alas...)
@@ -164,3 +167,84 @@ Sequence.metadata = {
164167
module: ['canvas'],
165168
nestedComponents: ['content'],
166169
}
170+
171+
export class Frame extends BaseFrame {
172+
constructor(options={}) {
173+
super(addCanvasDefaults({
174+
context: '<canvas></canvas>',
175+
...options,
176+
}))
177+
178+
// Push canvas to nested components
179+
if (!this.options.handMeDowns.includes('canvas')) {
180+
this.options.handMeDowns.push('canvas')
181+
}
182+
}
183+
184+
async onPrepare() {
185+
// Check that all nested components
186+
// are either flow components or
187+
// that they use the canvas
188+
const isFlowOrCanvasBased = (acc, c) =>
189+
acc && (
190+
c === this ||
191+
c instanceof Screen ||
192+
c instanceof Sequence ||
193+
c instanceof BaseSequence ||
194+
c instanceof Loop ||
195+
c instanceof Parallel
196+
)
197+
198+
const canvasBasedSubtree = reduce(this, isFlowOrCanvasBased, true)
199+
if (!canvasBasedSubtree) {
200+
throw 'CanvasFrame may only contain flow or canvas-based components'
201+
}
202+
203+
// TODO: This is largely lifted (with some adaptations)
204+
// from the html.Frame implementation. It would be great
205+
// to reduce duplication slightly. (the differences
206+
// are the allocation of the el option, and the
207+
// extraction of the canvas from the parsed context)
208+
209+
// Parse context HTML
210+
const parser = new DOMParser()
211+
this.internals.parsedContext = parser.parseFromString(
212+
this.options.context, 'text/html',
213+
)
214+
215+
// Extract canvas
216+
this.options.canvas = this.internals
217+
.parsedContext.querySelector('canvas')
218+
219+
if (!this.options.canvas) {
220+
throw 'No canvas found in context'
221+
}
222+
223+
// Set nested component el to the parent
224+
// element of the canvas, or the current el
225+
// (if the canvas is at the uppermost level
226+
// in the context HTML structure, and
227+
// therefore its parent in the virtual DOM
228+
// is a <body> element)
229+
this.options.content.options.el =
230+
this.options.canvas.parentElement === null ||
231+
this.options.canvas.parentElement.tagName === 'BODY'
232+
? this.options.el
233+
: this.options.canvas.parentElement
234+
235+
// Couple the run cycle of the frame to its content
236+
this.internals.contentEndHandler = () => this.end()
237+
this.options.content.on(
238+
'after:end',
239+
this.internals.contentEndHandler,
240+
)
241+
242+
// Prepare content
243+
await prepareNested([this.options.content], this)
244+
}
245+
}
246+
247+
Frame.metadata = {
248+
module: ['canvas'],
249+
nestedComponents: ['content'],
250+
}

packages/library/src/html.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ export class Frame extends Component {
191191
this.options.context, 'text/html',
192192
)
193193

194-
// Setup nested component to use the context
194+
// Setup nested component to operate within
195+
// the element addressed by the selector
195196
this.options.content.options.el = this.internals
196197
.parsedContext.querySelector(this.options.contextSelector)
197198

packages/library/src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Components
22
import { Component, Dummy } from './core'
3-
import { Screen as CanvasScreen, Sequence as CanvasSequence } from './canvas'
3+
import { Screen as CanvasScreen, Sequence as CanvasSequence,
4+
Frame as CanvasFrame } from './canvas'
45
import { Screen, Form, Frame } from './html'
56
import { Sequence, Parallel, Loop } from './flow'
67

@@ -27,6 +28,7 @@ export const core = {
2728
}
2829

2930
export const canvas = {
31+
Frame: CanvasFrame,
3032
Screen: CanvasScreen,
3133
Sequence: CanvasSequence,
3234
}

packages/library/test/canvas.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,5 +257,92 @@ describe('Canvas-based components', () => {
257257
})
258258
})
259259
})
260+
261+
describe('Frame', () => {
262+
263+
let f, s, a, b
264+
beforeEach(() => {
265+
a = new lab.canvas.Screen()
266+
b = new lab.canvas.Screen()
267+
s = new lab.flow.Sequence({
268+
content: [a, b],
269+
})
270+
f = new lab.canvas.Frame({
271+
content: s,
272+
el: document.createElement('div'),
273+
})
274+
})
275+
276+
it('provides canvas to nested components', () =>
277+
f.prepare().then(() => {
278+
assert.deepEqual(
279+
f.options.canvas,
280+
a.options.canvas,
281+
)
282+
})
283+
)
284+
285+
it('throws error if nested components are incompatible', () => {
286+
s.options.content.push(new lab.html.Screen())
287+
288+
return f.prepare().then(() => {
289+
assert.ok(false, 'Component should throw error during preparation')
290+
}).catch(err => {
291+
assert.equal(
292+
err,
293+
'CanvasFrame may only contain flow or canvas-based components',
294+
)
295+
})
296+
})
297+
298+
it('throws error if the context does not contain a canvas element', () => {
299+
f.options.context = '<div>Nope</div>'
300+
301+
return f.prepare().then(() => {
302+
assert.ok(false, 'Component should throw error during preparation')
303+
}).catch(err => {
304+
assert.equal(
305+
err,
306+
'No canvas found in context',
307+
)
308+
})
309+
})
310+
311+
it('hands down the canvas parent element', () => {
312+
f.options.context = '<div id="canvas-parent"><canvas></div>'
313+
314+
return f.run().then(() =>
315+
assert.equal(
316+
f.options.el.querySelector('div#canvas-parent'),
317+
a.options.el,
318+
)
319+
)
320+
})
321+
322+
it('hands down working el if canvas is at top level in context', () => {
323+
// ... as above, but without the wrapper
324+
f.options.context = '<canvas>'
325+
326+
return f.run().then(() =>
327+
assert.equal(
328+
f.options.el,
329+
a.options.el,
330+
)
331+
)
332+
})
333+
334+
it('ends with content', () =>
335+
// This is tested in more depth
336+
// in the HTML.Frame test suite
337+
f.run().then(
338+
() => s.end()
339+
).then(() => {
340+
assert.equal(
341+
f.status,
342+
3 // done
343+
)
344+
})
345+
)
346+
})
260347
})
261348
})

0 commit comments

Comments
 (0)