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

View file

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

View file

@ -4,122 +4,158 @@
const _commands_events = new Observer();
const commands = {
current: -1,
history: [],
undo(n = 1) {
for (var i = 0; i < n && this.current > -1; i++) {
this.history[this.current--].undo();
}
},
redo(n = 1) {
for (var i = 0; i < n && this.current + 1 < this.history.length; i++) {
this.history[++this.current].redo();
}
},
/** Global Commands Object */
const commands = makeReadOnly(
{
/** Current History Index Reader */
get current() {
return this._current;
},
/** Current History Index (private) */
_current: -1,
/** Command History (private) */
_history: [],
/** The types of commands we can run (private) */
_types: {},
/**
* These are basic commands that can be done/undone
*
* They must contain a 'run' method that performs the action 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.
*/
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;
/**
* Undoes the last commands in the history
*
* @param {number} n Number of actions to undo
*/
undo(n = 1) {
for (var i = 0; i < n && this.current > -1; i++) {
this._history[this._current--].undo();
}
},
/**
* 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({
id: entry.id,
name,
action: "undo",
action: "run",
state,
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,
current: commands._current,
});
return entry;
};
// 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: commands.current,
});
});
this._types[name] = command;
commands.history.splice(commands.current + 1);
}
commands.history.push(entry);
commands.current++;
entry.undo = undoWrapper;
entry.redo = redoWrapper;
_commands_events.emit({
id: entry.id,
name,
action: "run",
state,
current: commands.current,
});
return entry;
};
this.types[name] = command;
return command;
return command;
},
/**
* Runs a command
*
* @param {string} name The name of the command to run
* @param {string} title The display name of the command on the history panel view
* @param {any} options The options to be sent to the command to be run
*/
runCommand(name, title, options = null) {
this._types[name](title, options);
},
},
runCommand(name, title, options) {
this.types[name](title, options);
},
types: {},
};
"commands",
["_current"]
);
/**
* 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") {
Array.from(historyView.children).forEach((child) => {
if (
!commands.history.find((entry) => `hist-${entry.id}` === child.id)
!commands._history.find((entry) => `hist-${entry.id}` === child.id)
) {
console.log("Removing " + child.id);
historyView.removeChild(child);
@ -36,7 +36,7 @@
});
}
commands.history.forEach((entry, index) => {
commands._history.forEach((entry, index) => {
if (!document.getElementById(`hist-${entry.id}`)) {
console.log("Inserting " + entry.id);
historyView.appendChild(

View file

@ -1,30 +1,50 @@
/**
* Implementation of a simple Oberver Pattern for custom event handling
* Observer class
*/
function Observer() {
this.handlers = new Set();
}
class Observer {
/**
* 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) {
this.handlers.add(callback);
this._handlers.add(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) {
return this.handlers.delete(callback);
},
emit(msg) {
this.handlers.forEach(async (handler) => {
try {
await handler(msg);
} catch (e) {
console.warn("Observer failed to run handler");
console.warn(e);
}
});
},
};
return this._handlers.delete(callback);
}
/**
* Send a message to all observers
*
* @param {any} msg The message to send to the observers
*/
async emit(msg) {
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]
@ -62,16 +82,21 @@ class ProxyReadOnlySetError extends Error {}
/**
* 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
* @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, {
set: (obj, prop, value) => {
throw new ProxyReadOnlySetError(
`Tried setting the '${prop}' property on '${name}'`
);
if (!exceptions.some((v) => v === prop))
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
*
* @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
* @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, {
set: (obj, prop, value) => {
if (obj[prop] !== undefined)
if (obj[prop] !== undefined && !exceptions.some((v) => v === prop))
throw new ProxyWriteOnceSetError(
`Tried setting the '${prop}' property on '${name}' after it was already set`
);