added jsdoc to commands

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2022-11-28 12:17:07 -03:00
parent 8d0b44e36b
commit 54c381de8e
6 changed files with 204 additions and 149 deletions

View file

@ -241,7 +241,7 @@ div.prompt-wrapper > textarea:focus {
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
} }
.button-array > .button.tool { .button.tool {
background-color: rgb(0, 0, 50); background-color: rgb(0, 0, 50);
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
cursor: pointer; cursor: pointer;
@ -254,15 +254,15 @@ div.prompt-wrapper > textarea:focus {
margin-bottom: 5px; margin-bottom: 5px;
} }
.button-array > .button.tool:disabled { .button.tool:disabled {
background-color: #666 !important; background-color: #666 !important;
cursor: default; cursor: default;
} }
.button-array > .button.tool:hover { .button.tool:hover {
background-color: rgb(30, 30, 80); background-color: rgb(30, 30, 80);
} }
.button-array > .button.tool:active, .button.tool:active,
.button.tool.active { .button.tool.active {
background-color: rgb(60, 60, 130); background-color: rgb(60, 60, 130);
} }

View file

@ -284,7 +284,6 @@
</div> </div>
<!-- Base Libs --> <!-- Base Libs -->
<script src="js/error.js" type="text/javascript"></script>
<script src="js/util.js" type="text/javascript"></script> <script src="js/util.js" type="text/javascript"></script>
<script src="js/input.js" type="text/javascript"></script> <script src="js/input.js" type="text/javascript"></script>
<script src="js/commands.js" type="text/javascript"></script> <script src="js/commands.js" type="text/javascript"></script>

View file

@ -4,122 +4,158 @@
const _commands_events = new Observer(); const _commands_events = new Observer();
const commands = { /** Global Commands Object */
current: -1, const commands = makeReadOnly(
history: [], {
undo(n = 1) { /** Current History Index Reader */
for (var i = 0; i < n && this.current > -1; i++) { get current() {
this.history[this.current--].undo(); return this._current;
} },
}, /** Current History Index (private) */
redo(n = 1) { _current: -1,
for (var i = 0; i < n && this.current + 1 < this.history.length; i++) { /** Command History (private) */
this.history[++this.current].redo(); _history: [],
} /** The types of commands we can run (private) */
}, _types: {},
/** /**
* These are basic commands that can be done/undone * Undoes the last commands in the history
* *
* They must contain a 'run' method that performs the action the first time, * @param {number} n Number of actions to undo
* a 'undo' method that undoes that action and a 'redo' method that does the */
* action again, but without requiring parameters. 'redo' is by default the undo(n = 1) {
* same as 'run'. for (var i = 0; i < n && this.current > -1; i++) {
* this._history[this._current--].undo();
* The 'run' and 'redo' functions will receive a 'options' parameter which will be
* forwarded directly to the operation, and a 'state' parameter that
* can be used to store state for undoing things.
*
* The 'state' object will be passed to the 'undo' function as well.
*/
createCommand(name, run, undo, redo = run) {
const command = function runWrapper(title, options) {
// Create copy of options and state object
const copy = {};
Object.assign(copy, options);
const state = {};
const entry = {
id: guid(),
title,
state,
};
// Attempt to run command
try {
run(title, copy, state);
} catch (e) {
console.warn(`Error while running command '${name}' with options:`);
console.warn(copy);
console.warn(e);
return;
} }
},
/**
* Redoes the next commands in the history
*
* @param {number} n Number of actions to redo
*/
redo(n = 1) {
for (var i = 0; i < n && this.current + 1 < this._history.length; i++) {
this._history[++this._current].redo();
}
},
/**
* Creates a basic command, that can be done and undone
*
* They must contain a 'run' method that performs the action for the first time,
* a 'undo' method that undoes that action and a 'redo' method that does the
* action again, but without requiring parameters. 'redo' is by default the
* same as 'run'.
*
* The 'run' and 'redo' functions will receive a 'options' parameter which will be
* forwarded directly to the operation, and a 'state' parameter that
* can be used to store state for undoing things.
*
* The 'state' object will be passed to the 'undo' function as well.
*
* @param {string} name Command identifier (name)
* @param {(title: string, options: any, state: {[key: string]: any}) => void | Promise<void>} run A method that performs the action for the first time
* @param {(title: string, state: {[key: string]: any}) => } undo A method that reverses what the run method did
* @param {(title: string, options: any, state: {[key: string]: any}) => void | Promise<void>} redo A method that redoes the action after undone (default: run)
* @returns
*/
createCommand(name, run, undo, redo = run) {
const command = async function runWrapper(title, options) {
// Create copy of options and state object
const copy = {};
Object.assign(copy, options);
const state = {};
const entry = {
id: guid(),
title,
state,
};
// Attempt to run command
try {
await run(title, copy, state);
} catch (e) {
console.warn(`Error while running command '${name}' with options:`);
console.warn(copy);
console.warn(e);
return;
}
const undoWrapper = () => {
console.debug(`Undoing ${name}, currently ${this._current}`);
undo(title, state);
_commands_events.emit({
id: entry.id,
name,
action: "undo",
state,
current: this._current,
});
};
const redoWrapper = () => {
console.debug(`Redoing ${name}, currently ${this._current}`);
redo(title, copy, state);
_commands_events.emit({
id: entry.id,
name,
action: "redo",
state,
current: this._current,
});
};
// Add to history
if (commands._history.length > commands._current + 1) {
commands._history.forEach((entry, index) => {
if (index >= commands._current + 1)
_commands_events.emit({
id: entry.id,
name,
action: "deleted",
state,
current: this._current,
});
});
commands._history.splice(commands._current + 1);
}
commands._history.push(entry);
commands._current++;
entry.undo = undoWrapper;
entry.redo = redoWrapper;
const undoWrapper = () => {
console.debug(`Undoing ${name}, currently ${commands.current}`);
undo(title, state);
_commands_events.emit({ _commands_events.emit({
id: entry.id, id: entry.id,
name, name,
action: "undo", action: "run",
state, state,
current: commands.current, current: commands._current,
});
};
const redoWrapper = () => {
console.debug(`Redoing ${name}, currently ${commands.current}`);
redo(title, copy, state);
_commands_events.emit({
id: entry.id,
name,
action: "redo",
state,
current: commands.current,
}); });
return entry;
}; };
// Add to history this._types[name] = command;
if (commands.history.length > commands.current + 1) {
commands.history.forEach((entry, index) => {
if (index >= commands.current + 1)
_commands_events.emit({
id: entry.id,
name,
action: "deleted",
state,
current: commands.current,
});
});
commands.history.splice(commands.current + 1); return command;
} },
/**
commands.history.push(entry); * Runs a command
commands.current++; *
* @param {string} name The name of the command to run
entry.undo = undoWrapper; * @param {string} title The display name of the command on the history panel view
entry.redo = redoWrapper; * @param {any} options The options to be sent to the command to be run
*/
_commands_events.emit({ runCommand(name, title, options = null) {
id: entry.id, this._types[name](title, options);
name, },
action: "run",
state,
current: commands.current,
});
return entry;
};
this.types[name] = command;
return command;
}, },
runCommand(name, title, options) { "commands",
this.types[name](title, options); ["_current"]
}, );
types: {},
};
/** /**
* Draw Image Command, used to draw a Image to a context * Draw Image Command, used to draw a Image to a context

View file

@ -1,7 +0,0 @@
/**
* This is a file to configure custom errors
*/
/* Proxy Restriction Errors */
class ProxyReadOnlySetError extends Error {}
class ProxyWriteOnceSetError extends Error {}

View file

@ -28,7 +28,7 @@
if (message.action === "run") { if (message.action === "run") {
Array.from(historyView.children).forEach((child) => { Array.from(historyView.children).forEach((child) => {
if ( if (
!commands.history.find((entry) => `hist-${entry.id}` === child.id) !commands._history.find((entry) => `hist-${entry.id}` === child.id)
) { ) {
console.log("Removing " + child.id); console.log("Removing " + child.id);
historyView.removeChild(child); historyView.removeChild(child);
@ -36,7 +36,7 @@
}); });
} }
commands.history.forEach((entry, index) => { commands._history.forEach((entry, index) => {
if (!document.getElementById(`hist-${entry.id}`)) { if (!document.getElementById(`hist-${entry.id}`)) {
console.log("Inserting " + entry.id); console.log("Inserting " + entry.id);
historyView.appendChild( historyView.appendChild(

View file

@ -1,30 +1,50 @@
/** /**
* Implementation of a simple Oberver Pattern for custom event handling * Observer class
*/ */
function Observer() { class Observer {
this.handlers = new Set(); /**
} * List of handlers
* @type {Set<(msg: any) => void | Promise<void>>}
*/
_handlers = new Set();
Observer.prototype = { /**
// Adds handler for this message * Adds a observer to the events
*
* @param {(msg: any) => void | Promise<void>} callback The function to run when receiving a message
* @returns {(msg:any) => void | Promise<void>} The callback we received
*/
on(callback) { on(callback) {
this.handlers.add(callback); this._handlers.add(callback);
return callback; return callback;
}, }
/**
* Removes a observer
*
* @param {(msg: any) => void | Promise<void>} callback The function used to register the callback
* @returns {boolean} Whether the handler existed
*/
clear(callback) { clear(callback) {
return this.handlers.delete(callback); return this._handlers.delete(callback);
}, }
emit(msg) { /**
this.handlers.forEach(async (handler) => { * Send a message to all observers
try { *
await handler(msg); * @param {any} msg The message to send to the observers
} catch (e) { */
console.warn("Observer failed to run handler"); async emit(msg) {
console.warn(e); Promise.all(
} Array.from(this._handlers).map((handler) => async () => {
}); try {
}, await handler(msg);
}; } catch (e) {
console.warn("Observer failed to run handler");
console.warn(e);
}
})
);
}
}
/** /**
* Generates a simple UID in the format xxxx-xxxx-...-xxxx, with x being [0-9a-f] * Generates a simple UID in the format xxxx-xxxx-...-xxxx, with x being [0-9a-f]
@ -62,16 +82,21 @@ class ProxyReadOnlySetError extends Error {}
/** /**
* Makes a given object read-only; throws a ProxyReadOnlySetError exception if modification is attempted * Makes a given object read-only; throws a ProxyReadOnlySetError exception if modification is attempted
* *
* @param {any} obj Object to be proxied * @template T Object Type
*
* @param {T} obj Object to be proxied
* @param {string} name Name for logging purposes * @param {string} name Name for logging purposes
* @returns {any} Proxied object, intercepting write attempts * @param {string[]} exceptions Parameters excepted from this restriction
* @returns {T} Proxied object, intercepting write attempts
*/ */
function makeReadOnly(obj, name = "read-only object") { function makeReadOnly(obj, name = "read-only object", exceptions = []) {
return new Proxy(obj, { return new Proxy(obj, {
set: (obj, prop, value) => { set: (obj, prop, value) => {
throw new ProxyReadOnlySetError( if (!exceptions.some((v) => v === prop))
`Tried setting the '${prop}' property on '${name}'` throw new ProxyReadOnlySetError(
); `Tried setting the '${prop}' property on '${name}'`
);
obj[prop] = value;
}, },
}); });
} }
@ -81,14 +106,16 @@ class ProxyWriteOnceSetError extends Error {}
/** /**
* Makes a given object write-once; Attempts to overwrite an existing prop in the object will throw a ProxyWriteOnceSetError exception * Makes a given object write-once; Attempts to overwrite an existing prop in the object will throw a ProxyWriteOnceSetError exception
* *
* @param {any} obj Object to be proxied * @template T Object Type
* @param {T} obj Object to be proxied
* @param {string} name Name for logging purposes * @param {string} name Name for logging purposes
* @returns {any} Proxied object, intercepting write attempts * @param {string[]} exceptions Parameters excepted from this restriction
* @returns {T} Proxied object, intercepting write attempts
*/ */
function makeWriteOnce(obj, name = "write-once object") { function makeWriteOnce(obj, name = "write-once object", exceptions = []) {
return new Proxy(obj, { return new Proxy(obj, {
set: (obj, prop, value) => { set: (obj, prop, value) => {
if (obj[prop] !== undefined) if (obj[prop] !== undefined && !exceptions.some((v) => v === prop))
throw new ProxyWriteOnceSetError( throw new ProxyWriteOnceSetError(
`Tried setting the '${prop}' property on '${name}' after it was already set` `Tried setting the '${prop}' property on '${name}' after it was already set`
); );