Skip to content

Commit

Permalink
Create component module and install a div overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredjj3 committed Jun 22, 2024
1 parent a119236 commit 99b382d
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 130 deletions.
70 changes: 19 additions & 51 deletions site/src/components/Vexml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,16 @@ export type VexmlResult =

export const Vexml = (props: VexmlProps) => {
const musicXML = props.musicXML;
const mode = props.backend;
const backend = props.backend;
const config = props.config;
const onResult = props.onResult;
const onEvent = props.onEvent;

const divContainerRef = useRef<HTMLDivElement>(null);
const canvasContainerRef = useRef<HTMLCanvasElement>(null);

const divStyle: React.CSSProperties = {
display: mode === 'svg' ? 'block' : 'none',
userSelect: 'none',
msUserSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
};
const canvasStyle: React.CSSProperties = {
display: mode === 'canvas' ? 'block' : 'none',
userSelect: 'none',
msUserSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
};

const divWidth = useWidth(divContainerRef);
const canvasWidth = useWidth(canvasContainerRef);
const width = mode === 'svg' ? divWidth : canvasWidth;

const container = mode === 'svg' ? divContainerRef.current : canvasContainerRef.current;
const divRef = useRef<HTMLDivElement>(null);
const div = divRef.current;

const width = useWidth(divRef);

const [rendering, setRendering] = useState<vexml.Rendering | null>(null);

useEffect(() => {
Expand All @@ -64,9 +46,9 @@ export const Vexml = (props: VexmlProps) => {
handles.push(rendering.addEventListener('exit', onEvent));
handles.push(rendering.addEventListener('enter', onEvent));

if (container) {
if (div) {
const onEnter: vexml.EnterEventListener = (event) => {
container.style.cursor = 'pointer';
div.style.cursor = 'pointer';

// TODO: Create official wrapper around vexml Rendering* objects that allows users to color the notes.
const value = event.target.value;
Expand Down Expand Up @@ -104,7 +86,7 @@ export const Vexml = (props: VexmlProps) => {
}
};
const onExit: vexml.ExitEventListener = (event) => {
container.style.cursor = 'default';
div.style.cursor = 'default';

const value = event.target.value;
switch (value.type) {
Expand Down Expand Up @@ -147,14 +129,14 @@ export const Vexml = (props: VexmlProps) => {
return () => {
rendering.removeEventListener(...handles);
};
}, [rendering, container, onEvent]);
}, [rendering, div, onEvent]);

useEffect(() => {
if (!musicXML) {
onResult({ type: 'empty' });
return;
}
if (!container) {
if (!div) {
onResult({ type: 'none' });
return;
}
Expand All @@ -168,25 +150,16 @@ export const Vexml = (props: VexmlProps) => {

try {
rendering = vexml.Vexml.fromMusicXML(musicXML).render({
container,
element: div,
width,
backend,
config,
});
setRendering(rendering);

let element: HTMLCanvasElement | SVGElement;
if (container instanceof HTMLDivElement) {
element = container.firstElementChild as SVGElement;
// Now that the <svg> is created, we can set the style for screenshots.
element.style.backgroundColor = 'white';
element.style.pointerEvents = 'all';
} else if (container instanceof HTMLCanvasElement) {
// The <canvas> image background is transparent, and there's not much we can do to change that without
// significantly changing the vexml rendering.
element = container;
} else {
throw new Error(`invalid container: ${container}`);
}
const element = rendering.getVexflowElement();
// For screenshots, we want the background to be white.
element.style.backgroundColor = 'white';

onResult({
type: 'success',
Expand All @@ -208,12 +181,7 @@ export const Vexml = (props: VexmlProps) => {
return () => {
rendering?.destroy();
};
}, [musicXML, mode, config, width, container, onResult]);

return (
<>
<div className="w-100" ref={divContainerRef} style={divStyle}></div>
<canvas className="w-100" ref={canvasContainerRef} style={canvasStyle}></canvas>
</>
);
}, [musicXML, backend, config, width, div, onResult]);

return <div className="w-100" ref={divRef}></div>;
};
3 changes: 2 additions & 1 deletion src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
- [drawables](./drawables/README.md): Extend `vexflow`'s drawing capabilities.
- [spatial](./spatial/README.md): Provide data structures for spatial contexts.
- [events](./events/README.md): Exposes user interaction hooks.
- [cursors](./cursors/README.md): Different utilities for navigating a rendered music sheet.[]
- [cursors](./cursors/README.md): Different utilities for navigating a rendered music sheet.
- [components](./components/README.md): Creates UI components needed for vexml.
- `util`: Miscellaneous functionality that doesn't neatly fit into either library or needs to be shared.

[src/vexml.ts](./vexml.ts) is the entrypoint for MusicXML rendering.
13 changes: 13 additions & 0 deletions src/components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# components

## Intent

The intent of this component library is to provide a collection of reusable components for building user interfaces.

### Goals

- **DO** Insulate the rest of vexml from low-level HTML management.

### Non-goals

- **DO NOT** Be concerned with the actual vexflow rendering.
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './root';
77 changes: 77 additions & 0 deletions src/components/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* The root component that houses the vexflow renderings.
*
* The purpose of this class is to insulate low-level DOM manipulation from the rest of the codebase.
*/
export class Root {
private element: HTMLDivElement;

private constructor(element: HTMLDivElement) {
this.element = element;
}

static svg(parent: HTMLElement) {
return Root.render('svg', parent);
}

static canvas(parent: HTMLElement) {
return Root.render('canvas', parent);
}

private static render(type: 'svg' | 'canvas', parent: HTMLElement) {
const element = document.createElement('div');

element.classList.add('vexml-root');
element.style.position = 'relative';

const overlay = document.createElement('div');
overlay.classList.add('vexml-overlay');
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';

element.append(overlay);

switch (type) {
case 'svg':
const div = document.createElement('div');
div.classList.add('vexml-container');
div.classList.add('vexml-container-svg');
element.append(div);
break;
case 'canvas':
const canvas = document.createElement('canvas');
canvas.classList.add('vexml-container');
canvas.classList.add('vexml-container-canvas');
element.append(canvas);
break;
}

parent.append(element);

return new Root(element);
}

/** Returns the element that overlays the rendering. */
getOverlayElement(): HTMLDivElement {
return this.element.querySelector('.vexml-overlay') as HTMLDivElement;
}

/** Returns the element that is intended to be inputted to vexflow. */
getVexflowContainerElement(): HTMLDivElement | HTMLCanvasElement {
return this.element.querySelector('.vexml-container') as HTMLDivElement | HTMLCanvasElement;
}

/** Returns the element that vexflow rendered onto. */
getVexflowElement(): SVGElement | HTMLCanvasElement {
const container = this.getVexflowContainerElement();
return container instanceof HTMLDivElement ? (container.firstElementChild as SVGElement) : container;
}

/** Removes the element from the DOM. */
remove(): void {
this.element?.remove();
}
}
4 changes: 2 additions & 2 deletions src/cursors/pointcursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ export type CursorGetResult<T> = {

/** A object that tracks a spatial cursor, and returns the targets under it. */
export class PointCursor<T> {
private host: SVGElement | HTMLCanvasElement;
private host: HTMLElement;
private locator: spatial.PointLocator<T>;

constructor(host: SVGElement | HTMLCanvasElement, locator: spatial.PointLocator<T>) {
constructor(host: HTMLElement, locator: spatial.PointLocator<T>) {
this.host = host;
this.locator = locator;
}
Expand Down
59 changes: 21 additions & 38 deletions src/events/nativebridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,10 @@ import * as util from '@/util';
import { Topic } from './topic';
import { EventListener } from './types';

export type HostElement = SVGElement | HTMLCanvasElement;

type NativeEventName<T extends HostElement> = T extends SVGElement
? keyof SVGElementEventMap
: keyof HTMLElementEventMap;

type NativeEvent<T extends HostElement, N extends NativeEventName<T>> = T extends SVGElement
? SVGElementEventMap[N]
: HTMLElementEventMap[N];

export type NativeEventMap<T extends HostElement> = {
[N in NativeEventName<T>]: NativeEvent<T, N>;
};

export type NativeEventOpts<T extends HostElement> = {
[N in NativeEventName<T>]?: AddEventListenerOptions;
};

export type EventMapping<T extends HostElement, V extends string[]> = {
export type EventMapping<V extends string[]> = {
vexml: V;
native: {
[N in NativeEventName<T>]?: EventListener<NativeEvent<T, N>>;
[K in keyof HTMLElementEventMap]?: EventListener<HTMLElementEventMap[K]>;
};
};

Expand All @@ -35,21 +17,21 @@ export type EventMapping<T extends HostElement, V extends string[]> = {
* - Native events are only added to the host element when they are needed.
*/
export class NativeBridge<V extends string> {
private host: HostElement;
private mappings: EventMapping<HostElement, V[]>[];
private nativeEventTopic: Topic<NativeEventMap<HostElement>>;
private nativeEventOpts: NativeEventOpts<HostElement>;
private overlay: HTMLElement;
private mappings: EventMapping<V[]>[];
private nativeEventTopic: Topic<HTMLElementEventMap>;
private nativeEventOpts: { [K in keyof HTMLElementEventMap]?: AddEventListenerOptions };

// Handles for native event topic subscribers indexed by the vexml event name.
private handles: { [K in V]?: number[] } = {};

constructor(opts: {
host: HostElement;
mappings: EventMapping<HostElement, V[]>[];
nativeEventTopic: Topic<NativeEventMap<HostElement>>;
nativeEventOpts: NativeEventOpts<HostElement>;
overlay: HTMLElement;
mappings: EventMapping<V[]>[];
nativeEventTopic: Topic<HTMLElementEventMap>;
nativeEventOpts: { [K in keyof HTMLElementEventMap]?: AddEventListenerOptions };
}) {
this.host = opts.host;
this.overlay = opts.overlay;
this.mappings = opts.mappings;
this.nativeEventTopic = opts.nativeEventTopic;
this.nativeEventOpts = opts.nativeEventOpts;
Expand Down Expand Up @@ -77,13 +59,13 @@ export class NativeBridge<V extends string> {
this.handles[vexmlEventName] ??= [];

for (const native of Object.entries(mapping.native)) {
const nativeEventName = native[0] as NativeEventName<HostElement>;
const nativeEventListener = native[1] as EventListener<NativeEvent<HostElement, NativeEventName<HostElement>>>;
const nativeEventName = native[0] as keyof HTMLElementEventMap;
const nativeEventListener = native[1];

// Enforce only a single listener per native event. vexml is intended to consume the event through the
// nativeEventTopic. That way, we only run the native callbacks that we need to run.
if (!this.nativeEventTopic.hasSubscribers(nativeEventName)) {
this.host.addEventListener(nativeEventName, this.publishNativeEvent, this.nativeEventOpts[nativeEventName]);
this.overlay.addEventListener(nativeEventName, this.publishNativeEvent, this.nativeEventOpts[nativeEventName]);
}
const handle = this.nativeEventTopic.subscribe(nativeEventName, nativeEventListener);
this.handles[vexmlEventName]!.push(handle);
Expand All @@ -110,10 +92,14 @@ export class NativeBridge<V extends string> {
delete this.handles[vexmlEventName];

for (const native of Object.entries(mapping.native)) {
const nativeEventName = native[0] as NativeEventName<HostElement>;
const nativeEventName = native[0] as keyof HTMLElementEventMap;

if (!this.nativeEventTopic.hasSubscribers(nativeEventName)) {
this.host.removeEventListener(nativeEventName, this.publishNativeEvent, this.nativeEventOpts[nativeEventName]);
this.overlay.removeEventListener(
nativeEventName,
this.publishNativeEvent,
this.nativeEventOpts[nativeEventName]
);
}
}
}
Expand All @@ -133,10 +119,7 @@ export class NativeBridge<V extends string> {
* deactivated.
*/
private publishNativeEvent = (event: Event) => {
this.nativeEventTopic.publish(
event.type as NativeEventName<HostElement>,
event as NativeEvent<HostElement, NativeEventName<HostElement>>
);
this.nativeEventTopic.publish(event.type as keyof HTMLElementEventMap, event);
return false;
};

Expand Down
10 changes: 5 additions & 5 deletions src/rendering/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class EventMappingFactory {
this.topic = topic;
}

create(inputType: InputType): events.EventMapping<events.HostElement, Array<keyof EventMap>>[] {
create(inputType: InputType): events.EventMapping<Array<keyof EventMap>>[] {
switch (inputType) {
case 'mouse':
return [this.mousePress(), this.mouseEgress()];
Expand All @@ -77,7 +77,7 @@ export class EventMappingFactory {
}
}

private mousePress(): events.EventMapping<events.HostElement, ['click', 'longpress']> {
private mousePress(): events.EventMapping<['click', 'longpress']> {
let timeout = 0 as unknown as NodeJS.Timeout;
let isPending = false;
let lastMouseDownInvocation = Symbol();
Expand Down Expand Up @@ -121,7 +121,7 @@ export class EventMappingFactory {
};
}

private mouseEgress(): events.EventMapping<events.HostElement, ['enter', 'exit']> {
private mouseEgress(): events.EventMapping<['enter', 'exit']> {
let lastEvent: MouseEvent | null = null;

return {
Expand Down Expand Up @@ -156,7 +156,7 @@ export class EventMappingFactory {
};
}

private touchPress(): events.EventMapping<events.HostElement, ['click', 'longpress']> {
private touchPress(): events.EventMapping<['click', 'longpress']> {
let timeout = 0 as unknown as NodeJS.Timeout;
let isPending = false;
let lastTouchStartInvocation = Symbol();
Expand Down Expand Up @@ -204,7 +204,7 @@ export class EventMappingFactory {
};
}

private touchEgress(): events.EventMapping<events.HostElement, ['enter', 'exit']> {
private touchEgress(): events.EventMapping<['enter', 'exit']> {
let lastEnteredEvent: TouchEvent | null = null;

return {
Expand Down
Loading

0 comments on commit 99b382d

Please sign in to comment.