mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
WebBrowserComponent: Add native integration helper Javascript library
This commit is contained in:
parent
5f638157f7
commit
ec92ce82b6
3 changed files with 503 additions and 0 deletions
112
modules/juce_gui_extra/native/javascript/check_native_interop.js
Normal file
112
modules/juce_gui_extra/native/javascript/check_native_interop.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
if (
|
||||
typeof window.__JUCE__ !== "undefined" &&
|
||||
typeof window.__JUCE__.getAndroidUserScripts !== "undefined" &&
|
||||
typeof window.inAndroidUserScriptEval === "undefined"
|
||||
) {
|
||||
window.inAndroidUserScriptEval = true;
|
||||
eval(window.__JUCE__.getAndroidUserScripts());
|
||||
delete window.inAndroidUserScriptEval;
|
||||
}
|
||||
|
||||
{
|
||||
if (typeof window.__JUCE__ === "undefined") {
|
||||
console.warn(
|
||||
"The 'window.__JUCE__' object is undefined." +
|
||||
" Native integration features will not work." +
|
||||
" Defining a placeholder 'window.__JUCE__' object."
|
||||
);
|
||||
|
||||
window.__JUCE__ = {
|
||||
postMessage: function () {},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.__JUCE__.initialisationData === "undefined") {
|
||||
window.__JUCE__.initialisationData = {
|
||||
__juce__platform: [],
|
||||
__juce__functions: [],
|
||||
__juce__registeredGlobalEventIds: [],
|
||||
__juce__sliders: [],
|
||||
__juce__toggles: [],
|
||||
__juce__comboBoxes: [],
|
||||
};
|
||||
}
|
||||
|
||||
class ListenerList {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
this.listenerId = 0;
|
||||
}
|
||||
|
||||
addListener(fn) {
|
||||
const newListenerId = this.listenerId++;
|
||||
this.listeners.set(newListenerId, fn);
|
||||
return newListenerId;
|
||||
}
|
||||
|
||||
removeListener(id) {
|
||||
if (this.listeners.has(id)) {
|
||||
this.listeners.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
callListeners(payload) {
|
||||
for (const [, value] of this.listeners) {
|
||||
value(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EventListenerList {
|
||||
constructor() {
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
addEventListener(eventId, fn) {
|
||||
if (!this.eventListeners.has(eventId))
|
||||
this.eventListeners.set(eventId, new ListenerList());
|
||||
|
||||
const id = this.eventListeners.get(eventId).addListener(fn);
|
||||
|
||||
return [eventId, id];
|
||||
}
|
||||
|
||||
removeEventListener([eventId, id]) {
|
||||
if (this.eventListeners.has(eventId)) {
|
||||
this.eventListeners.get(eventId).removeListener(id);
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(eventId, object) {
|
||||
if (this.eventListeners.has(eventId))
|
||||
this.eventListeners.get(eventId).callListeners(object);
|
||||
}
|
||||
}
|
||||
|
||||
class Backend {
|
||||
constructor() {
|
||||
this.listeners = new EventListenerList();
|
||||
}
|
||||
|
||||
addEventListener(eventId, fn) {
|
||||
return this.listeners.addEventListener(eventId, fn);
|
||||
}
|
||||
|
||||
removeEventListener([eventId, id]) {
|
||||
this.listeners.removeEventListener(eventId, id);
|
||||
}
|
||||
|
||||
emitEvent(eventId, object) {
|
||||
window.__JUCE__.postMessage(
|
||||
JSON.stringify({ eventId: eventId, payload: object })
|
||||
);
|
||||
}
|
||||
|
||||
emitByBackend(eventId, object) {
|
||||
this.listeners.emitEvent(eventId, JSON.parse(object));
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window.__JUCE__.backend === "undefined")
|
||||
window.__JUCE__.backend = new Backend();
|
||||
}
|
||||
387
modules/juce_gui_extra/native/javascript/index.js
Normal file
387
modules/juce_gui_extra/native/javascript/index.js
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
import "./check_native_interop.js";
|
||||
|
||||
class PromiseHandler {
|
||||
constructor() {
|
||||
this.lastPromiseId = 0;
|
||||
this.promises = new Map();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(
|
||||
"__juce__complete",
|
||||
({ promiseId, result }) => {
|
||||
if (this.promises.has(promiseId)) {
|
||||
this.promises.get(promiseId).resolve(result);
|
||||
this.promises.delete(promiseId);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createPromise() {
|
||||
const promiseId = this.lastPromiseId++;
|
||||
const result = new Promise((resolve, reject) => {
|
||||
this.promises.set(promiseId, { resolve: resolve, reject: reject });
|
||||
});
|
||||
return [promiseId, result];
|
||||
}
|
||||
}
|
||||
|
||||
const promiseHandler = new PromiseHandler();
|
||||
|
||||
/**
|
||||
* Returns a function object that calls a function registered on the JUCE backend and forwards all
|
||||
* parameters to it.
|
||||
*
|
||||
* The provided name should be the same as the name argument passed to
|
||||
* WebBrowserComponent::Options.withNativeFunction() on the backend.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getNativeFunction(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__functions.includes(name))
|
||||
console.warn(
|
||||
`Creating native function binding for '${name}', which is unknown to the backend`
|
||||
);
|
||||
|
||||
const f = function () {
|
||||
const [promiseId, result] = promiseHandler.createPromise();
|
||||
|
||||
window.__JUCE__.backend.emitEvent("__juce__invoke", {
|
||||
name: name,
|
||||
params: Array.prototype.slice.call(arguments),
|
||||
resultId: promiseId,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
class ListenerList {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
this.listenerId = 0;
|
||||
}
|
||||
|
||||
addListener(fn) {
|
||||
const newListenerId = this.listenerId++;
|
||||
this.listeners.set(newListenerId, fn);
|
||||
return newListenerId;
|
||||
}
|
||||
|
||||
removeListener(id) {
|
||||
if (this.listeners.has(id)) {
|
||||
this.listeners.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
callListeners(payload) {
|
||||
for (const [, value] of this.listeners) {
|
||||
value(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BasicControl_valueChangedEventId = "valueChanged";
|
||||
const BasicControl_propertiesChangedId = "propertiesChanged";
|
||||
|
||||
class SliderState {
|
||||
constructor(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__sliders.includes(name))
|
||||
console.warn(
|
||||
"Creating SliderState for '" +
|
||||
name +
|
||||
"', which is unknown to the backend"
|
||||
);
|
||||
|
||||
this.name = name;
|
||||
this.identifier = "__juce__slider" + this.name;
|
||||
this.scaledValue = 0;
|
||||
this.properties = {
|
||||
start: 0,
|
||||
end: 1,
|
||||
skew: 1,
|
||||
name: "",
|
||||
label: "",
|
||||
numSteps: 100,
|
||||
interval: 0,
|
||||
};
|
||||
this.valueChangedEvent = new ListenerList();
|
||||
this.propertiesChangedEvent = new ListenerList();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
||||
this.handleEvent(event)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: "requestInitialUpdate",
|
||||
});
|
||||
}
|
||||
|
||||
setNormalisedValue(newValue) {
|
||||
this.scaledValue = this.snapToLegalValue(
|
||||
this.normalisedToScaledValue(newValue)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: BasicControl_valueChangedEventId,
|
||||
value: this.scaledValue,
|
||||
});
|
||||
}
|
||||
|
||||
sliderDragStarted() {}
|
||||
|
||||
sliderDragEnded() {}
|
||||
|
||||
handleEvent(event) {
|
||||
if (event.eventType == BasicControl_valueChangedEventId) {
|
||||
this.scaledValue = event.value;
|
||||
this.valueChangedEvent.callListeners();
|
||||
}
|
||||
if (event.eventType == BasicControl_propertiesChangedId) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let { eventType: _, ...rest } = event;
|
||||
this.properties = rest;
|
||||
this.propertiesChangedEvent.callListeners();
|
||||
}
|
||||
}
|
||||
|
||||
getScaledValue() {
|
||||
return this.scaledValue;
|
||||
}
|
||||
|
||||
getNormalisedValue() {
|
||||
return Math.pow(
|
||||
(this.scaledValue - this.properties.start) /
|
||||
(this.properties.end - this.properties.start),
|
||||
this.properties.skew
|
||||
);
|
||||
}
|
||||
|
||||
normalisedToScaledValue(normalisedValue) {
|
||||
return (
|
||||
Math.pow(normalisedValue, 1 / this.properties.skew) *
|
||||
(this.properties.end - this.properties.start) +
|
||||
this.properties.start
|
||||
);
|
||||
}
|
||||
|
||||
snapToLegalValue(value) {
|
||||
const interval = this.properties.interval;
|
||||
|
||||
if (interval == 0) return value;
|
||||
|
||||
const start = this.properties.start;
|
||||
const clamp = (val, min = 0, max = 1) => Math.max(min, Math.min(max, val));
|
||||
|
||||
return clamp(
|
||||
start + interval * Math.floor((value - start) / interval + 0.5),
|
||||
this.properties.start,
|
||||
this.properties.end
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sliderStates = new Map();
|
||||
|
||||
for (const sliderName of window.__JUCE__.initialisationData.__juce__sliders)
|
||||
sliderStates.set(sliderName, new SliderState(sliderName));
|
||||
|
||||
/**
|
||||
* Returns a SliderState object that is connected to the backend WebSliderRelay object that was
|
||||
* created with the same name argument.
|
||||
*
|
||||
* To register a WebSliderRelay object create one with the right name and add it to the
|
||||
* WebBrowserComponent::Options struct using withOptionsFrom.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getSliderState(name) {
|
||||
if (!sliderStates.has(name)) sliderStates.set(name, new SliderState(name));
|
||||
|
||||
return sliderStates.get(name);
|
||||
}
|
||||
|
||||
class ToggleState {
|
||||
constructor(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__toggles.includes(name))
|
||||
console.warn(
|
||||
"Creating ToggleState for '" +
|
||||
name +
|
||||
"', which is unknown to the backend"
|
||||
);
|
||||
|
||||
this.name = name;
|
||||
this.identifier = "__juce__toggle" + this.name;
|
||||
this.value = false;
|
||||
this.properties = {
|
||||
name: "",
|
||||
};
|
||||
this.valueChangedEvent = new ListenerList();
|
||||
this.propertiesChangedEvent = new ListenerList();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
||||
this.handleEvent(event)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: "requestInitialUpdate",
|
||||
});
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
setValue(newValue) {
|
||||
this.value = newValue;
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: BasicControl_valueChangedEventId,
|
||||
value: this.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
if (event.eventType == BasicControl_valueChangedEventId) {
|
||||
this.value = event.value;
|
||||
this.valueChangedEvent.callListeners();
|
||||
}
|
||||
if (event.eventType == BasicControl_propertiesChangedId) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let { eventType: _, ...rest } = event;
|
||||
this.properties = rest;
|
||||
this.propertiesChangedEvent.callListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleStates = new Map();
|
||||
|
||||
for (const name of window.__JUCE__.initialisationData.__juce__toggles)
|
||||
toggleStates.set(name, new ToggleState(name));
|
||||
|
||||
/**
|
||||
* Returns a ToggleState object that is connected to the backend WebToggleButtonRelay object that was
|
||||
* created with the same name argument.
|
||||
*
|
||||
* To register a WebToggleButtonRelay object create one with the right name and add it to the
|
||||
* WebBrowserComponent::Options struct using withOptionsFrom.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getToggleState(name) {
|
||||
if (!toggleStates.has(name)) toggleStates.set(name, new ToggleState(name));
|
||||
|
||||
return toggleStates.get(name);
|
||||
}
|
||||
|
||||
class ComboBoxState {
|
||||
constructor(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__comboBoxes.includes(name))
|
||||
console.warn(
|
||||
"Creating ComboBoxState for '" +
|
||||
name +
|
||||
"', which is unknown to the backend"
|
||||
);
|
||||
|
||||
this.name = name;
|
||||
this.identifier = "__juce__comboBox" + this.name;
|
||||
this.value = 0.0;
|
||||
this.properties = {
|
||||
name: "",
|
||||
choices: [],
|
||||
};
|
||||
this.valueChangedEvent = new ListenerList();
|
||||
this.propertiesChangedEvent = new ListenerList();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
||||
this.handleEvent(event)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: "requestInitialUpdate",
|
||||
});
|
||||
}
|
||||
|
||||
getChoiceIndex() {
|
||||
return Math.round(this.value * (this.properties.choices.length - 1));
|
||||
}
|
||||
|
||||
setChoiceIndex(index) {
|
||||
const numItems = this.properties.choices.length;
|
||||
this.value = numItems > 1 ? index / (numItems - 1) : 0.0;
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: BasicControl_valueChangedEventId,
|
||||
value: this.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
if (event.eventType == BasicControl_valueChangedEventId) {
|
||||
this.value = event.value;
|
||||
this.valueChangedEvent.callListeners();
|
||||
}
|
||||
if (event.eventType == BasicControl_propertiesChangedId) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let { eventType: _, ...rest } = event;
|
||||
this.properties = rest;
|
||||
this.propertiesChangedEvent.callListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const comboBoxStates = new Map();
|
||||
|
||||
for (const name of window.__JUCE__.initialisationData.__juce__comboBoxes)
|
||||
comboBoxStates.set(name, new ComboBoxState(name));
|
||||
|
||||
/**
|
||||
* Returns a ComboBoxState object that is connected to the backend WebComboBoxRelay object that was
|
||||
* created with the same name argument.
|
||||
*
|
||||
* To register a WebComboBoxRelay object create one with the right name and add it to the
|
||||
* WebBrowserComponent::Options struct using withOptionsFrom.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getComboBoxState(name) {
|
||||
if (!comboBoxStates.has(name))
|
||||
comboBoxStates.set(name, new ComboBoxState(name));
|
||||
|
||||
return comboBoxStates.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a platform-specific prefix to the path to ensure that a request sent to this address will
|
||||
* be received by the backend's ResourceProvider.
|
||||
* @param {String} path
|
||||
*/
|
||||
function getBackendResourceAddress(path) {
|
||||
const platform =
|
||||
window.__JUCE__.initialisationData.__juce__platform.length > 0
|
||||
? window.__JUCE__.initialisationData.__juce__platform[0]
|
||||
: "";
|
||||
|
||||
if (platform == "windows" || platform == "android")
|
||||
return "https://juce.backend/" + path;
|
||||
|
||||
if (platform == "macos" || platform == "ios" || platform == "linux")
|
||||
return "juce://juce.backend/" + path;
|
||||
|
||||
console.warn(
|
||||
"getBackendResourceAddress() called, but no JUCE native backend is detected."
|
||||
);
|
||||
return path;
|
||||
}
|
||||
|
||||
export {
|
||||
getNativeFunction,
|
||||
getSliderState,
|
||||
getToggleState,
|
||||
getComboBoxState,
|
||||
getBackendResourceAddress,
|
||||
};
|
||||
4
modules/juce_gui_extra/native/javascript/package.json
Normal file
4
modules/juce_gui_extra/native/javascript/package.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "juce-framework-frontend",
|
||||
"version": "7.0.7"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue