added jsdoc to commands
Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
parent
8d0b44e36b
commit
54c381de8e
6 changed files with 204 additions and 149 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
244
js/commands.js
244
js/commands.js
|
@ -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
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
/**
|
|
||||||
* This is a file to configure custom errors
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Proxy Restriction Errors */
|
|
||||||
class ProxyReadOnlySetError extends Error {}
|
|
||||||
class ProxyWriteOnceSetError extends Error {}
|
|
|
@ -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(
|
||||||
|
|
89
js/util.js
89
js/util.js
|
@ -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`
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue