mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 22:06:00 +00:00
Merge branch 'main' of https://github.com/HeyPuter/puter into main
This commit is contained in:
commit
1f7a4782d8
@ -30,6 +30,7 @@ import path from "./lib/path.js";
|
|||||||
import UIContextMenu from './UI/UIContextMenu.js';
|
import UIContextMenu from './UI/UIContextMenu.js';
|
||||||
import update_mouse_position from './helpers/update_mouse_position.js';
|
import update_mouse_position from './helpers/update_mouse_position.js';
|
||||||
import item_icon from './helpers/item_icon.js';
|
import item_icon from './helpers/item_icon.js';
|
||||||
|
import { PROCESS_IPC_ATTACHED } from './definitions.js';
|
||||||
|
|
||||||
window.ipc_handlers = {};
|
window.ipc_handlers = {};
|
||||||
/**
|
/**
|
||||||
@ -92,18 +93,44 @@ window.addEventListener('message', async (event) => {
|
|||||||
// New IPC handlers should be registered here.
|
// New IPC handlers should be registered here.
|
||||||
// Do this by calling `register_ipc_handler` of IPCService.
|
// Do this by calling `register_ipc_handler` of IPCService.
|
||||||
if ( window.ipc_handlers.hasOwnProperty(event.data.msg) ) {
|
if ( window.ipc_handlers.hasOwnProperty(event.data.msg) ) {
|
||||||
|
const services = globalThis.services;
|
||||||
|
const svc_process = services.get('process');
|
||||||
|
|
||||||
|
// Add version info to old puter.js messages
|
||||||
|
// (and coerce them into the format of new ones)
|
||||||
|
if ( event.data.$ === undefined ) {
|
||||||
|
event.data.$ = 'puter-ipc';
|
||||||
|
event.data.v = 1;
|
||||||
|
event.data.parameters = {...event.data};
|
||||||
|
delete event.data.parameters.msg;
|
||||||
|
delete event.data.parameters.appInstanceId;
|
||||||
|
delete event.data.parameters.env;
|
||||||
|
delete event.data.parameters.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
// The IPC context contains information about the call
|
// The IPC context contains information about the call
|
||||||
|
const iframe = window.iframe_for_app_instance(
|
||||||
|
event.data.appInstanceID);
|
||||||
|
const process = svc_process.get_by_uuid(event.data.appInstanceID);
|
||||||
const ipc_context = {
|
const ipc_context = {
|
||||||
appInstanceId: event.data.appInstanceID,
|
caller: {
|
||||||
|
process: process,
|
||||||
|
app: {
|
||||||
|
appInstanceID: event.data.appInstanceID,
|
||||||
|
iframe,
|
||||||
|
window: $el_parent_window,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registered IPC handlers are an object with a `handle()`
|
// Registered IPC handlers are an object with a `handle()`
|
||||||
// method. We call it "spec" here, meaning specification.
|
// method. We call it "spec" here, meaning specification.
|
||||||
const spec = window.ipc_handlers[event.data.msg];
|
const spec = window.ipc_handlers[event.data.msg];
|
||||||
await spec.handler(event.data, { msg_id, ipc_context });
|
let retval = await spec.handler(
|
||||||
|
event.data.parameters, { msg_id, ipc_context });
|
||||||
|
|
||||||
|
puter.util.rpc.send(iframe.contentWindow, msg_id, retval);
|
||||||
|
|
||||||
// Early-return to avoid redundant invokation of any
|
|
||||||
// legacy IPC handler.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,25 +139,16 @@ window.addEventListener('message', async (event) => {
|
|||||||
// READY
|
// READY
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
if(event.data.msg === 'READY'){
|
if(event.data.msg === 'READY'){
|
||||||
$(target_iframe).attr('data-appUsesSDK', 'true');
|
const services = globalThis.services;
|
||||||
|
const svc_process = services.get('process');
|
||||||
// If we were waiting to launch this as a child app, report to the parent that it succeeded.
|
const process = svc_process.get_by_uuid(event.data.appInstanceID);
|
||||||
window.report_app_launched(event.data.appInstanceID, { uses_sdk: true });
|
|
||||||
|
|
||||||
// Send any saved broadcasts to the new app
|
|
||||||
globalThis.services.get('broadcast').sendSavedBroadcastsTo(event.data.appInstanceID);
|
|
||||||
|
|
||||||
// If `window-active` is set (meanign the window is focused), focus the window one more time
|
|
||||||
// this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown)
|
|
||||||
if($el_parent_window.hasClass('window-active')){
|
|
||||||
$el_parent_window.focusWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
process.ipc_status = PROCESS_IPC_ATTACHED;
|
||||||
}
|
}
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
// windowFocused
|
// windowFocused
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
else if(event.data.msg === 'windowFocused'){
|
if(event.data.msg === 'windowFocused'){
|
||||||
// TODO: Respond to this
|
// TODO: Respond to this
|
||||||
}
|
}
|
||||||
//--------------------------------------------------------
|
//--------------------------------------------------------
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { AdvancedBase } from "@heyputer/putility";
|
||||||
|
|
||||||
export class Service {
|
export class Service {
|
||||||
construct (o) {
|
construct (o) {
|
||||||
this.$puter = {};
|
this.$puter = {};
|
||||||
@ -33,20 +35,36 @@ export class Service {
|
|||||||
export const PROCESS_INITIALIZING = { i18n_key: 'initializing' };
|
export const PROCESS_INITIALIZING = { i18n_key: 'initializing' };
|
||||||
export const PROCESS_RUNNING = { i18n_key: 'running' };
|
export const PROCESS_RUNNING = { i18n_key: 'running' };
|
||||||
|
|
||||||
|
export const PROCESS_IPC_PENDING = { i18n_key: 'pending' };
|
||||||
|
export const PROCESS_IPC_NA = { i18n_key: 'N/A' };
|
||||||
|
export const PROCESS_IPC_ATTACHED = { i18n_key: 'attached' };
|
||||||
|
|
||||||
// Something is cloning these objects, so '===' checks don't work.
|
// Something is cloning these objects, so '===' checks don't work.
|
||||||
// To work around this, the `i` property is used to compare them.
|
// To work around this, the `i` property is used to compare them.
|
||||||
export const END_SOFT = { i: 0, end: true, i18n_key: 'end_soft' };
|
export const END_SOFT = { i: 0, end: true, i18n_key: 'end_soft' };
|
||||||
export const END_HARD = { i: 1, end: true, i18n_key: 'end_hard' };
|
export const END_HARD = { i: 1, end: true, i18n_key: 'end_hard' };
|
||||||
|
|
||||||
export class Process {
|
export class Process extends AdvancedBase{
|
||||||
|
static PROPERTIES = {
|
||||||
|
status: () => PROCESS_INITIALIZING,
|
||||||
|
ipc_status: () => PROCESS_IPC_PENDING,
|
||||||
|
}
|
||||||
constructor ({ uuid, parent, name, meta }) {
|
constructor ({ uuid, parent, name, meta }) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.meta = meta;
|
this.meta = meta;
|
||||||
this.references = {};
|
this.references = {};
|
||||||
|
|
||||||
this.status = PROCESS_INITIALIZING;
|
Object.defineProperty(this.references, 'iframe', {
|
||||||
|
get: () => {
|
||||||
|
// note: Might eventually make sense to make the
|
||||||
|
// fn on window call here instead.
|
||||||
|
return window.iframe_for_app_instance(this.uuid);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this._construct();
|
this._construct();
|
||||||
}
|
}
|
||||||
@ -111,6 +129,11 @@ export class PortalProcess extends Process {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
send (channel, object, context) {
|
||||||
|
const target = this.references.iframe.contentWindow;
|
||||||
|
// NEXT: ...
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export class PseudoProcess extends Process {
|
export class PseudoProcess extends Process {
|
||||||
_construct () { this.type_ = 'ui' }
|
_construct () { this.type_ = 'ui' }
|
||||||
|
@ -358,25 +358,16 @@ const launch_app = async (options)=>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
const el = await el_win;
|
||||||
const el = await el_win;
|
process.references.el_win = el;
|
||||||
$(el).on('remove', () => {
|
process.chstatus(PROCESS_RUNNING);
|
||||||
const svc_process = globalThis.services.get('process');
|
|
||||||
svc_process.unregister(process.uuid);
|
|
||||||
|
|
||||||
// If it's a non-sdk app, report that it launched and closed.
|
$(el).on('remove', () => {
|
||||||
// FIXME: This is awkward. Really, we want some way of knowing when it's launched and reporting that immediately instead.
|
const svc_process = globalThis.services.get('process');
|
||||||
const $app_iframe = $(el).find('.window-app-iframe');
|
svc_process.unregister(process.uuid);
|
||||||
if ($app_iframe.attr('data-appUsesSdk') !== 'true') {
|
});
|
||||||
window.report_app_launched(process.uuid, { uses_sdk: false });
|
|
||||||
// We also have to report an extra close event because the real one was sent already
|
|
||||||
window.report_app_closed(process.uuid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
process.references.el_win = el;
|
return process;
|
||||||
process.chstatus(PROCESS_RUNNING);
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default launch_app;
|
export default launch_app;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Service } from "../definitions.js";
|
import { PROCESS_IPC_ATTACHED, Service } from "../definitions.js";
|
||||||
import launch_app from "../helpers/launch_app.js";
|
import launch_app from "../helpers/launch_app.js";
|
||||||
|
|
||||||
export class ExecService extends Service {
|
export class ExecService extends Service {
|
||||||
@ -14,20 +14,60 @@ export class ExecService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This method is exposed to apps via IPCService.
|
// This method is exposed to apps via IPCService.
|
||||||
launchApp ({ app_name, args }, { ipc_context, msg_id } = {}) {
|
async launchApp ({ app_name, args }, { ipc_context, msg_id } = {}) {
|
||||||
|
const app = ipc_context?.caller?.app;
|
||||||
|
const process = ipc_context?.caller?.process;
|
||||||
|
|
||||||
// This mechanism will be replated with xdrpc soon
|
// This mechanism will be replated with xdrpc soon
|
||||||
const child_instance_id = window.uuidv4();
|
const child_instance_id = window.uuidv4();
|
||||||
window.child_launch_callbacks[child_instance_id] = {
|
|
||||||
parent_instance_id: event.data.appInstanceID,
|
|
||||||
launch_msg_id: msg_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
// The "body" of this method is in a separate file
|
// The "body" of this method is in a separate file
|
||||||
launch_app({
|
const child_process = await launch_app({
|
||||||
name: app_name,
|
name: app_name,
|
||||||
args: args ?? {},
|
args: args ?? {},
|
||||||
parent_instance_id: ipc_context?.appInstanceId,
|
parent_instance_id: app?.appInstanceID,
|
||||||
uuid: child_instance_id,
|
uuid: child_instance_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const send_child_launched_msg = (...a) => {
|
||||||
|
const parent_iframe = process?.references?.iframe;
|
||||||
|
parent_iframe.contentWindow.postMessage({
|
||||||
|
msg: 'childAppLaunched',
|
||||||
|
original_msg_id: msg_id,
|
||||||
|
child_instance_id,
|
||||||
|
...a,
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
child_process.onchange('ipc_status', value => {
|
||||||
|
if ( value !== PROCESS_IPC_ATTACHED ) return;
|
||||||
|
|
||||||
|
$(child_process.references.iframe).attr('data-appUsesSDK', 'true');
|
||||||
|
|
||||||
|
send_child_launched_msg({ uses_sdk: true });
|
||||||
|
|
||||||
|
// Send any saved broadcasts to the new app
|
||||||
|
globalThis.services.get('broadcast').sendSavedBroadcastsTo(child_instance_id);
|
||||||
|
|
||||||
|
// If `window-active` is set (meanign the window is focused), focus the window one more time
|
||||||
|
// this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown)
|
||||||
|
if(child_process.el_win.hasClass('window-active')){
|
||||||
|
child_process.el_win.focusWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(child_process.references.el_win).on('remove', () =>{
|
||||||
|
const parent_iframe = process?.references?.iframe;
|
||||||
|
if ($(parent_iframe).attr('data-appUsesSdk') !== 'true') {
|
||||||
|
send_child_launched_msg({ uses_sdk: false });
|
||||||
|
// We also have to report an extra close event because the real one was sent already
|
||||||
|
window.report_app_closed(child_process.uuid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
appInstanceID: child_instance_id,
|
||||||
|
usesSDK: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Since `Symbol` is not clonable, we use a UUID to identify RPCs.
|
// Since `Symbol` is not clonable, we use a UUID to identify RPCs.
|
||||||
const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
|
export const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CallbackManager is used to manage callbacks for RPCs.
|
* The CallbackManager is used to manage callbacks for RPCs.
|
||||||
@ -12,7 +12,7 @@ const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
|
|||||||
* the functions that are being called remotely.
|
* the functions that are being called remotely.
|
||||||
*/
|
*/
|
||||||
export class CallbackManager {
|
export class CallbackManager {
|
||||||
#messageId = 0;
|
#messageId = 1;
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.callbacks = new Map();
|
this.callbacks = new Map();
|
||||||
@ -27,7 +27,6 @@ export class CallbackManager {
|
|||||||
attach_to_source (source) {
|
attach_to_source (source) {
|
||||||
source.addEventListener('message', event => {
|
source.addEventListener('message', event => {
|
||||||
const { data } = event;
|
const { data } = event;
|
||||||
debugger;
|
|
||||||
if (data && typeof data === 'object' && data.$SCOPE === $SCOPE) {
|
if (data && typeof data === 'object' && data.$SCOPE === $SCOPE) {
|
||||||
const { id, args } = data;
|
const { id, args } = data;
|
||||||
const callback = this.callbacks.get(id);
|
const callback = this.callbacks.get(id);
|
||||||
|
@ -17,6 +17,16 @@ class AppConnection extends EventListener {
|
|||||||
// Whether the target app uses the Puter SDK, and so accepts messages
|
// Whether the target app uses the Puter SDK, and so accepts messages
|
||||||
// (Closing and close events will still function.)
|
// (Closing and close events will still function.)
|
||||||
#usesSDK;
|
#usesSDK;
|
||||||
|
|
||||||
|
static from (values, { appInstanceID, messageTarget }) {
|
||||||
|
const connection = new AppConnection(
|
||||||
|
messageTarget,
|
||||||
|
appInstanceID,
|
||||||
|
values.appInstanceID,
|
||||||
|
values.usesSDK
|
||||||
|
);
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) {
|
constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) {
|
||||||
super([
|
super([
|
||||||
@ -161,6 +171,33 @@ class UI extends EventListener {
|
|||||||
value: dehydrator.dehydrate(value),
|
value: dehydrator.dehydrate(value),
|
||||||
}, '*');
|
}, '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ipc_stub = async function ({
|
||||||
|
callback,
|
||||||
|
method,
|
||||||
|
parameters,
|
||||||
|
}) {
|
||||||
|
let p, resolve;
|
||||||
|
await new Promise(done_setting_resolve => {
|
||||||
|
p = new Promise(resolve_ => {
|
||||||
|
resolve = resolve_;
|
||||||
|
done_setting_resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if ( ! resolve ) debugger;
|
||||||
|
const callback_id = this.util.rpc.registerCallback(resolve);
|
||||||
|
this.messageTarget?.postMessage({
|
||||||
|
$: 'puter-ipc', v: 2,
|
||||||
|
appInstanceID: this.appInstanceID,
|
||||||
|
env: this.env,
|
||||||
|
msg: method,
|
||||||
|
parameters,
|
||||||
|
uuid: callback_id,
|
||||||
|
}, '*');
|
||||||
|
const ret = await p;
|
||||||
|
if ( callback ) callback(ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
constructor (appInstanceID, parentInstanceID, appID, env, util) {
|
constructor (appInstanceID, parentInstanceID, appID, env, util) {
|
||||||
const eventNames = [
|
const eventNames = [
|
||||||
@ -339,7 +376,7 @@ class UI extends EventListener {
|
|||||||
}
|
}
|
||||||
// Determine if this is a response to a previous message and if so, is there
|
// Determine if this is a response to a previous message and if so, is there
|
||||||
// a callback function for this message? if answer is yes to both then execute the callback
|
// a callback function for this message? if answer is yes to both then execute the callback
|
||||||
else if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
|
else if(e.data.original_msg_id !== undefined && this.#callbackFunctions[e.data.original_msg_id]){
|
||||||
if(e.data.msg === 'fileOpenPicked'){
|
if(e.data.msg === 'fileOpenPicked'){
|
||||||
// 1 item returned
|
// 1 item returned
|
||||||
if(e.data.items.length === 1){
|
if(e.data.items.length === 1){
|
||||||
@ -400,11 +437,6 @@ class UI extends EventListener {
|
|||||||
// execute callback
|
// execute callback
|
||||||
this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file));
|
this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file));
|
||||||
}
|
}
|
||||||
else if (e.data.msg === 'childAppLaunched') {
|
|
||||||
// execute callback with a new AppConnection to the child
|
|
||||||
const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id, e.data.uses_sdk);
|
|
||||||
this.#callbackFunctions[e.data.original_msg_id](connection);
|
|
||||||
}
|
|
||||||
else{
|
else{
|
||||||
// execute callback
|
// execute callback
|
||||||
this.#callbackFunctions[e.data.original_msg_id](e.data);
|
this.#callbackFunctions[e.data.original_msg_id](e.data);
|
||||||
@ -903,16 +935,20 @@ class UI extends EventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns a Promise<AppConnection>
|
// Returns a Promise<AppConnection>
|
||||||
launchApp = function(appName, args, callback) {
|
launchApp = async function launchApp(app_name, args, callback) {
|
||||||
return new Promise((resolve) => {
|
const app_info = await this.#ipc_stub({
|
||||||
// if appName is an object and args is not set, then appName is actually args
|
method: 'launchApp',
|
||||||
if (typeof appName === 'object' && !args) {
|
callback,
|
||||||
args = appName;
|
parameters: {
|
||||||
appName = undefined;
|
app_name,
|
||||||
}
|
args,
|
||||||
|
},
|
||||||
this.#postMessageWithCallback('launchApp', resolve, { app_name: appName, args });
|
});
|
||||||
})
|
|
||||||
|
return AppConnection.from(app_info, {
|
||||||
|
appInstanceID: this.appInstanceID,
|
||||||
|
messageTarget: this.messageTarget,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parentApp() {
|
parentApp() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CallbackManager, Dehydrator, Hydrator } from "../lib/xdrpc";
|
import { $SCOPE, CallbackManager, Dehydrator, Hydrator } from "../lib/xdrpc";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Util module exposes utilities within puter.js itself.
|
* The Util module exposes utilities within puter.js itself.
|
||||||
@ -27,4 +27,12 @@ class UtilRPC {
|
|||||||
getHydrator ({ target }) {
|
getHydrator ({ target }) {
|
||||||
return new Hydrator({ target });
|
return new Hydrator({ target });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerCallback (resolve) {
|
||||||
|
return this.callbackManager.register_callback(resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
send (target, id, ...args) {
|
||||||
|
target.postMessage({ $SCOPE, id, args }, '*');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,10 @@ class BasicBase {
|
|||||||
_get_merged_static_array (key) {
|
_get_merged_static_array (key) {
|
||||||
const chain = this._get_inheritance_chain();
|
const chain = this._get_inheritance_chain();
|
||||||
const values = [];
|
const values = [];
|
||||||
|
let last = null;
|
||||||
for ( const cls of chain ) {
|
for ( const cls of chain ) {
|
||||||
if ( cls[key] ) {
|
if ( cls[key] && cls[key] !== last ) {
|
||||||
|
last = cls[key];
|
||||||
values.push(...cls[key]);
|
values.push(...cls[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,6 +41,7 @@ class BasicBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_get_merged_static_object (key) {
|
_get_merged_static_object (key) {
|
||||||
|
// TODO: check objects by reference - same object in a subclass shouldn't count
|
||||||
const chain = this._get_inheritance_chain();
|
const chain = this._get_inheritance_chain();
|
||||||
const values = {};
|
const values = {};
|
||||||
for ( const cls of chain ) {
|
for ( const cls of chain ) {
|
||||||
|
@ -25,7 +25,7 @@ class FeatureBase extends BasicBase {
|
|||||||
this._ = {
|
this._ = {
|
||||||
features: this._get_merged_static_array('FEATURES'),
|
features: this._get_merged_static_array('FEATURES'),
|
||||||
};
|
};
|
||||||
|
|
||||||
for ( const feature of this._.features ) {
|
for ( const feature of this._.features ) {
|
||||||
feature.install_in_instance(
|
feature.install_in_instance(
|
||||||
this,
|
this,
|
||||||
|
@ -17,14 +17,23 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
name: 'Properties',
|
||||||
install_in_instance: (instance) => {
|
install_in_instance: (instance) => {
|
||||||
const properties = instance._get_merged_static_object('PROPERTIES');
|
const properties = instance._get_merged_static_object('PROPERTIES');
|
||||||
|
|
||||||
|
instance.onchange = (name, callback) => {
|
||||||
|
instance.__properties[name].listeners.push(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.__properties = {};
|
||||||
|
|
||||||
for ( const k in properties ) {
|
for ( const k in properties ) {
|
||||||
if ( typeof properties[k] === 'function' ) {
|
const state = {
|
||||||
instance[k] = properties[k]();
|
definition: properties[k],
|
||||||
continue;
|
listeners: [],
|
||||||
}
|
value: undefined,
|
||||||
|
};
|
||||||
|
instance.__properties[k] = state;
|
||||||
|
|
||||||
if ( typeof properties[k] === 'object' ) {
|
if ( typeof properties[k] === 'object' ) {
|
||||||
// This will be supported in the future.
|
// This will be supported in the future.
|
||||||
@ -32,7 +41,29 @@ module.exports = {
|
|||||||
`is not a supported property specification.`);
|
`is not a supported property specification.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
instance[k] = properties[k];
|
let value = (() => {
|
||||||
|
if ( typeof properties[k] === 'function' ) {
|
||||||
|
return properties[k]();
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties[k];
|
||||||
|
})();
|
||||||
|
|
||||||
|
Object.defineProperty(instance, k, {
|
||||||
|
get: () => {
|
||||||
|
return state.value;
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
for ( const listener of instance.__properties[k].listeners ) {
|
||||||
|
listener(value, {
|
||||||
|
old_value: instance[k],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
state.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user