intrigue
v0.1.3
Published
N/A
Downloads
3
Readme
Intrigue
A Mousetrap-based, LightTable inspired contextual binding system.
Example
Intrigue can be used two ways, depending on your needs. If you only require statically defined bindings, you can use the Director
directly, which only manages context bindings. If you need to programmatically change bindings on the fly, you can use the Intrigue
engine, which manages updating the director as bindings are changed.
In either case, you'll want to provide a dispatcher
and an element_context
function. The dispatcher
maps action strings like test.save
to function calls, and the element_context
function allows the Director
to 1) Find the right target for the triggered context in the document hierarchy, and 2) Automatically update the active context stack as you interact with the UI. For the element_context
function to work, you need to provide a tabindex
on the elements representing contexts.
// Provide a dispatcher tied into your action system.
// (A simple nested map of actions is provided as an example below).
let my_actions = {
nav: {
next(elem, event) {
console.log("NEXT!");
}
}
};
let dispatcher = (action, target, event) => {
// resolve action string (e.g. "nav.next" in a nested map of actions.
let cur = my_actions as any;
for (let part of action.split(".")) {
if (!cur) continue;
cur = cur[part];
}
let elem = get_my_data_from_element(target);
if(typeof cur === "function") cur(elem, event);
else console.warn("Unhandled dispatch!", action);
};
// Specify how the context attached to an element (if any) should be retrieved.
// (The default implementation is shown as an example below).
interface ContextTarget extends HTMLElement { context?: number; }
let element_context_get = function(element: ContextTarget) {
return element.context;
}
Using the director directly (for simple, static bindings)
import {Director} from "intrigue";
// Create a simple inlined map of contexts.
let contexts = [
{name: "root", bindings: {}},
{name: "test", parent: 0, bindings: {
"ctrl+s": "test.save",
"ctrl+l": "test.load",
}}
];
let director = new Director(contexts, dispatcher, element_context_get);
// Manually enter the "test" state.
director.enter(1);
// Now try "ctrl+s"!
Using the binding engine (allows you to dynamically update and create bindings on the fly)
import {Intrigue} from "intrigue";
// Create the engine
let intrigue = new Intrigue();
// You'll need to set the director up in the same way you do above:
intrigue.director.dispatcher = dispatcher;
intrigue.director.element_context = element_context_get;
// The Intrigue engine will automatically reload bindings as they change, but if you need to update
// your UI or trigger other effects when the bindings are changed, you can provide a handler for it.
intrigue.on_change = () => console.log("Changed!")
// Load your saved bindings.
// You can also programmatically save directly in the engine format using `intrigue.save()` and `intrigue.load()`.
// Note that `save` and `load` do *not* utilize the condensed format, which is provided for editing convenience.
intrigue.load_condensed({
groups: [{name: "test", actions: ["save", "load"]}],
contexts: [
{
name: "test",
bindings: {
"ctrl+s": "test.save",
"ctrl+l": "test.load"
}
}
]
});
// Manually enter the context with ID 1 ("test").
intrigue.director.enter(1);
// Now try "ctrl+s"!
Documentation
Director
Types
export type Dispatcher = (action: string, target: Element, event: KeyboardEvent, args?:any[], context_id: number) => any;
export type ElementContextGetter = (element: Element) => number | undefined;
export interface InlinedContext { name?: string; parent?: number; bindings: {[shortcut: string]: string | undefined} }
export type InlinedContexts = (InlinedContext | undefined)[];
Public API
constructor(public contexts: InlinedContexts, public dispatcher: Dispatcher, public element_context: ElementContextGetter)
Construct a new Director
.
destroy()
Completely unbind the Director
from the DOM.
@NOTE: If using some form of HMR, make sure to destroy the old instance before constructing a new one, or both will fire.
clear()
Reset active contexts and clear shortcuts.
resolve_fqn(fqn: string): number | undefined
Resolve a Fully Qualified Name (e.g., "parent.child"
to a nested context id). Returns undefined if no such context exists.
in(context_id: number): boolean
in_fqn(fqn: string): boolean
Test whether the given context is currently active.
enter(context_id: number)
enter_by_fqn(fqn: string)
Manually add the given context to the active stack.
@NOTE: This can activate the same context multiple times. Bindings will still only trigger once for whichever the most specific context is for a shortcut.
leave(context_id: number)
leave_by_fqn(fqn: string)
Manually pop the given context to the active stack.
@NOTE: If this context has been activated multiple times, this will pop the last occurrence of it.
rebind()
Re-apply shortcut bindings for the currently active contexts. You should not need to do this unless you are manually changing the context map without using an Intrigue
instance.
get_triggered_context(shortcut: string)
Get the id for the most specific context which binds this shortcut (if any).
Intrigue
Types
export interface RawActionInfo {
name?: string;
group: number;
}
export interface ActionInfo extends RawActionInfo {
id: number;
fqn?: string;
bindings: number[];
}
export interface RawGroupInfo {
name?: string;
parent?: number;
}
export interface GroupInfo extends RawGroupInfo {
id: number;
fqn?: string;
actions: number[];
subgroups: number[];
}
export interface RawBindingInfo {
context: number;
action?: number;
shortcuts: string[];
}
export interface BindingInfo extends RawBindingInfo {
id: number;
}
export interface RawContextInfo {
name?: string;
parent?: number;
}
export interface ContextInfo extends RawContextInfo {
id: number;
subcontexts: number[];
bindings: number[];
}
export interface IntrigueSave {
actions: (ActionInfo | undefined)[];
groups: (GroupInfo | undefined)[];
bindings: (BindingInfo | undefined)[];
contexts: (ContextInfo | undefined)[];
}
export interface CondensedGroup {
name: string;
subgroups?: CondensedGroup[];
actions?: string[];
__parent?: number; // @NOTE: For loader use only, ignore this.
}
export interface CondensedContext {
name: string;
bindings: {[shortcut: string]: string | undefined};
subcontexts?: CondensedContext[];
}
export interface IntrigueCondensedSave {
groups: CondensedGroup[];
contexts: CondensedContext[];
}
Public API
director: Director
on_change?: () => any
reload() // Rebind the director instance after changes have been made and trigger `on_change`
schedule_reload() // Schedule a debounced `reload`
schedule_change() // Schedule a debounced `on_change` (for changes that don't require rebinding the director)
save(): IntrigueSave // Creates a raw engine image
load(save: IntrigueSave) // Loads a raw engine image
load_condensed(save: IntrigueCondensedSave) // Loads a human-friendly "condensed" image
// Editing utilities
action = {
get(action_id: number): ActionInfo;
add(group_id: number): number;
remove(action_id: number);
name(action_id: number, name: string);
group(action_id: number, group_id: number);
};
group = {
get(group_id: number): GroupInfo;
get_roots(): GroupInfo[];
add(parent_id?: number): number;
remove(group_id: number);
name(group_id: number, name: string | undefined);
parent(group_id: number, parent_id: number);
};
binding = {
get(binding_id: number): BindingInfo;
add(context_id: number): number;
remove(binding_id: number);
action(binding_id: number, action: number);
shortcut: {
add(binding_id: number, shortcut: string);
remove(binding_id: number, shortcut: string);
replace(binding_id: number, ix: number, shortcut: string);
set(binding_id: number, shortcuts: string[]);
}
};
context = {
get(context_id: number): ContextInfo;
get_roots(): ContextInfo[];
add(parent_id?: number): number;
remove(context_id: number);
name(context_id: number, name: string | undefined);
parent(context_id: number, parent_id: number);
};
WIP
This doesn't feel right. Seems like the common cases are going to be accepting one of a few common patterns.
Maybe we just provide those as builtin options in [[DIGIT]]
, [[INT]]
, [[DECIMAL]]
? What about the case of enumerations?
Shortcut Modifiers
[[\d]]
- RegExp match character[[\d]]+
- RegExp match multiple characters (NOTE: If used as the terminal key in a binding sequence, a timeout will be used to determine the end of input).[[-]]?
- RegExp maybe match character!!
- Don't prevent default on the event triggering this shortcut
Examples
f
- When f is pressed!
- When shift+1 is pressedx [[-]]? [[\d]]+
- When x is pressed followed by a positive or negative number