npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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 pressed
  • x [[-]]? [[\d]]+ - When x is pressed followed by a positive or negative number