Skip to content

bUxEE/dirigo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Dirigo

Configurable Vue 3 plugin that registers only the directives you choose. Zero runtime dependencies (peer: vue). UI-library agnostic (DOM APIs only).

Current release: 1.2.0 on npm · Source: github.com/bUxEE/dirigo

Install

npm install @woptima/dirigo

Quick start

import { createApp } from 'vue'
import App from './App.vue'
import { Dirigo } from '@woptima/dirigo'

const app = createApp(App)

app.use(Dirigo, {
  directives: 'all', // or e.g. ['click-copy', 'html-safe', 'case']
  prefix: '', // optional e.g. 'vd' → v-vd-click-copy
  directiveConfig: {
    imageOverlay: {
      // backdropStyle: 'rgba(0,0,0,0.1)',
      // component: MyImageModal, // props: value (image URL), onClose
    },
    clickCopy: {
      // toastComponent: MyCopyToast, // prop: message (label text)
      // duration: 2500,
    },
  },
})

app.mount('#app')

Local registration

import { clickCopyDirective } from '@woptima/dirigo'

app.directive('click-copy', clickCopyDirective)

Options

Option Type Description
directives DirectiveName[] | 'all' Which directives to register globally.
prefix string? Prepended to each name (prefix: 'vd'vd-click-copy).
directiveConfig DirigoDirectiveConfig? Shared UI defaults (e.g. imageOverlay, clickCopy toast).

Directives overview

Name Purpose
size-vars Publishes --{id}-width / --{id}-height on :root (element needs id).
next-pad Sets next sibling padding-top to this element’s height.
case Enforces 'uppercase' or 'lowercase' on <input> / <textarea> + syncs v-model (v-case="'uppercase'" / v-case="'lowercase'").
type-value On blur, normalizes v-model (kind: text | number | integer | float); invalid numeric strings clear to ''.
input-type Key filter modes: integer, decimal, digits, phone, numpad, slug.
transitioning Adds transitioning class around CSS transitions (optional property filter).
drop-file onDrop(files) + drag-over class.
paste-file { onPaste(files) } for clipboard files (no custom DOM events).
image-overlay Full-screen image viewer on <img> or a wrapper: default modal, or { component, componentProps }. Optional callback-only: (src) => void or { onOpen } without component / overlay: true.
click-copy Copy on click + toast (value, label, optional toastComponent, duration).
link-copy Copy URL / href + toast (same toast options as click-copy).
html-safe Like v-html with default allowlist sanitizer; optional sanitize(html).
sticky Applies position: sticky + offsets (top, bottom, zIndex, …).
strip-html-paste Plain-text paste for inputs / contenteditable.
scroll-lock Locks body scroll (ref-count); optional { target }.
scroll-spy Scroll-linked “active” section: viewport = trigger-line box (default: host); root = scroll target (inferred: nearest vertical scroll container from that box when omitted).
media-class Adds classes for coarse, reducedMotion, dark media queries.
hash-scroll Scrolls to id when location.hash matches.
select-on-focus Select-all on focus.
skeleton Loading shimmer overlay; host keeps layout while content is visibility: hidden; optional theme, delay, custom component.
debounce-input Debounces input; listen for debounced-input custom event.
view-classes view-visible when ≥1px in view; directional classes (entering-top, entering-bottom, leaving-top, leaving-bottom) while scrolling and partially visible.
more-text Truncates text with Show more / Show less (maxLength, labels).
is-scrolling has-vertical-scroll / has-horizontal-scroll when overflow exists; is-scrolling-vertical / is-scrolling-horizontal when scrolled (scrollTop > 0 / scrollLeft > 0, LTR).
page-visibility-class Tab hidden/visible classes.
img-fallback Placeholder src on <img> error.
keybind Hotkey map (esc, ctrl+enter, …) → handlers; optional focusWithin / disabled.
infinite-scroll IntersectionObserver on scroll root: { load, disabled?, distance?, rootMargin?, throttleMs? }.
longpress Pointer long-press (handler, duration?, preventContextMenu?).
truncate Truncates element textContent to length (number or { length, ellipsis? }).
drag-select Click-drag box selection with { items, selected, key? }; children use data-{key} matching items (Ctrl/Meta toggles, Shift range).
empty-state When items is empty: hides default slot and shows empty UI (text, icon, component, ctaText / onCta).
form-lock When binding is true, disables inputs/buttons inside the host; restores prior disabled when unlocked.
error-focus On first invalid (HTML5 validation), focus + optional smooth scrollIntoView / shake; optional ariaInvalid / findFirstInvalid.
float-to Toggle: animate host to match a target element’s screen rect (fixed + optional transition); ResizeObserver keeps it aligned. Second click fades out and restores layout.

Details & bindings

html-safe

  • Object: { html: string, sanitize?: (html: string) => string } or allowlist options for the built-in sanitizer.
  • String shorthand: same HTML string, default sanitizer.
  • Security: default sanitizer is best-effort; for untrusted HTML prefer server-side filtering and/or pass DOMPurify (your dependency) via sanitize.

image-overlay

  • Host: put v-image-overlay on the <img> (uses that image’s src / currentSrc) or on a wrapper (uses the first nested <img>). Optional binding { src: '…' } overrides the URL.
  • Modal (default): click opens a built-in full-screen overlay (backdrop, centered image constrained to the viewport, white × control top-right, Escape closes). Override globally with directiveConfig.imageOverlay (component, backdropStyle, padding) or per binding: { component: MyModal, componentProps?: { … } }. Custom components receive value (image URL string) and onClose.
  • Callback-only: v-image-overlay="(src) => …" or { onOpen } without component and without overlay: true — runs the handler instead of opening the modal.

click-copy / link-copy toast

  • Per binding: { value, label, toastComponent?, duration? }.
  • Global: directiveConfig.clickCopy.toastComponent (Vue component with prop message for the label) and duration (ms).

paste-file / drop-file

<input v-paste-file="{ onPaste: (files) => console.log(files) }" />
<div v-drop-file="{ onDrop: (files) => upload(files) }"></div>

case

Required binding: 'uppercase' or 'lowercase' (or a ref holding either). Invalid values log a dev warning and do not transform.

<input v-model="code" v-case="'uppercase'" />
<textarea v-model="note" v-case="'lowercase'" />

type-value / input-type

  • type-value: on blur, normalizes the value: integer only accepts a full signed integer string (12abc or asdasdas → cleared); float / number use Number() on the whole string (12abc → cleared). Empty input becomes ''. HTML inputs cannot bind NaN; in script use e.g. const n = num === '' ? NaN : Number(num) if you need it.
  • input-type: blocks disallowed keydown characters (optional; separate from type-value).
<input v-model="n" v-type-value="{ kind: 'float' }" />
<input v-input-type="{ mode: 'phone' }" />

more-text

<p v-more-text="{ maxLength: 120, moreLabel: 'Show more', lessLabel: 'Show less' }">Long text …</p>

is-scrolling

  • Overflow: has-vertical-scroll / has-horizontal-scroll when content overflows that axis.
  • Scrolled: is-scrolling-vertical when scrollTop > 0; is-scrolling-horizontal when scrollLeft > 0 (LTR origin). Optional overrides: { hasVertical, hasHorizontal, scrolledVertical, scrolledHorizontal }. Deprecated: vertical / horizontal map to the has- names.

keybind

<div v-keybind="{ esc: onClose, 'ctrl+enter': onSubmit, focusWithin: true }"></div>

Uses capture on window. Modifiers: ctrl, meta/cmd, alt, shift. Keys: esc, enter, single letters, etc.

infinite-scroll

Host must be the scroll container (overflow: auto / scroll). A sentinel is appended; when it intersects (near bottom), load runs. Set disabled: true while fetching.

longpress

v-longpress="onLongPress" or v-longpress="{ handler: onLongPress, duration: 600 }".

truncate

v-truncate="40" or v-truncate="{ length: 40, ellipsis: '…' }" (best with static text; reactive copy changes may need :key on the host).

float-to

  • Binding: { target, transition?, trigger? }. target: HTMLElement, Vue Ref<HTMLElement | null>, or a CSS selector (via document.querySelector). transition: CSS transition for top / left / width / height (default ~400ms cubic-bezier). trigger: selector within the host for the clickable node; omit to use the host as the click target.
  • Behavior: first click captures the host’s getBoundingClientRect() (viewport), moves the host to document.body for that flight, applies position: fixed with a high z-index, then animates top / left / width / height to the target’s viewport rect (same coordinate system). While flying and docked, the host has class floated-to (export FLOAT_TO_DOCKED_CLASS) for hooks or styling; it is removed when returning or unmounting. Without the body move, any ancestor transform / filter / perspective would make fixed use a different containing block than the viewport, so pixel rects would be wrong. After the return fade, the host is inserted back at its original DOM position. While docked, the host tracks the target via ResizeObserver (element box changes) plus window resize, document scroll (capture) so window resize and scroll (which move getBoundingClientRect() without firing RO) still sync; visualViewport resize / scroll are included when available. transitionend from descendants is ignored for the fade so nested controls do not reset the host early.
  • Templates: like v-scroll-spy, avoid v-float-to="{ target: myRef }" — Vue unwraps myRef in inline objects. Use computed(() => ({ target: myRef, … })) and v-float-to="opts".

error-focus

Place on a <form> (or an element inside one). Listens for the bubbling invalid event (HTML5 constraint validation: required, pattern, setCustomValidity, etc.).

  • scroll: true (default) — after focus(), calls scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }). false — no scroll into view.
  • shake: optional short CSS shake on the focused field (default true).
  • ariaInvalid: if the form has no :invalid element but there is [aria-invalid="true"], focus that node instead of the event target. Useful when a library marks errors only with ARIA (some Vuetify setups) while the browser still emitted invalid from a related control.
  • findFirstInvalid(form): return the HTMLElement to focus yourself (VeeValidate, custom rules, errors on wrappers). Use this when validation is not expressed as native validity on the right node, or when you need a specific field order.

Vuetify / VeeValidate / other libs: There is no automatic “library detection”. The directive uses the DOM: the invalid event and optionally :invalid, [aria-invalid="true"], or your findFirstInvalid. Purely JS-driven forms that never call reportValidity() or never produce invalid events will not trigger this unless you wire validation so those events fire, or you use findFirstInvalid and ensure your submit path still surfaces an invalid event (e.g. hidden native inputs) — otherwise focus the field from your validator and skip this directive for that path.

skeleton

  • Binding: a Vue Component, or { loading?, component?, props?, theme?, delay?, minDuration?, debug? }. loading drives the overlay (boolean or ref; omitted means loading). Pass a component to replace the built-in shimmer; its props merge with props (and the built-in receives a theme prop when using the default skeleton).
  • theme: dark | light — palette for the built-in shimmer only (dark default: slate-style gradient; light: light gray for pale surfaces). Ignored if you pass a custom component (unless you forward theme yourself).
  • delay / minDuration: ms before showing the overlay after loading stays on, and minimum time the overlay stays visible (defaults 120 / 200). debug: log show/hide timing to the console.
  • Built-in shimmer animation runs on a ~2s cycle.

scroll-spy

  • Binding: { active, selector?, offset?, root?, viewport? }. active (required) is a Ref<string> updated with the id of the active block. Templates: do not write v-scroll-spy="{ active: myRef, … }" — Vue unwraps myRef inside the object. Use const opts = computed(() => ({ active: myRef, … })) and v-scroll-spy="opts". selector queries from the host (default :scope > *); only elements with an id participate. offset (default 0.4) places the trigger line at that fraction of the viewport box height. viewport: omithost (the element with the directive); 'body' → full-page visual viewport (window.innerHeight); otherwise a selector / element / ref. root: omitnearest vertical scroll container walking up from the resolved viewport node (same starting point as the default trigger box: the host); viewport: 'body'window; root: null → force window; or pass an element / ref. If the scrollable panel is only a child of the host, set viewport (or root) to that element — inference does not search down into descendants.
  • Logic: an element is “crossing” when rect.top <= trigger && rect.bottom > trigger (using getBoundingClientRect inside requestAnimationFrame). If several cross, scrolling down picks the last, scrolling up the first. If none cross, uses the block closest above the line, else the first block.
  • Listeners: scroll on root or window; resize on window to refresh the cached node list.

scroll-lock

Mounted element triggers body scroll lock until unmounted (nested modals: ref-counted).

Migration from legacy registerDirectives

Old New
vue-directives-plus (package) @woptima/dirigo — plugin export Dirigo, createDirigo, types DirigoOptions, DirigoDirectiveConfig
uppercase v-case with 'uppercase' or 'lowercase'
numberValue v-type-value
numberInput / phoneInput / … v-input-type with mode
v-transition v-transitioning
pasteFile v-paste-file + { onPaste } (no custom DOM event)
height-style use v-size-vars + CSS var(--id-height)
scroll-shadow* handle in CSS / app
scrolling-vertical / scrolling-horizontal (old is-scrolling) has-vertical-scroll / has-horizontal-scroll + is-scrolling-vertical / is-scrolling-horizontal

Development

npm install
npm run playground
npm test
npm run build

Marketing site (site/)

Not included in the npm package. Separate Vue app: home, playground (reuses playground/App.vue), docs/API table.

npm run site:dev
npm run site:build
npm run site:preview

See docs/REPOSITORY_LAYOUT.md for deploying to your domain and splitting public library vs private site across two repos.

Publish to npm

npm run release:npm:dry          # test + build + npm publish --dry-run (no version bump)
npm run release:npm              # patch version + publish (local git tag/commit from npm version)
RELEASE_NO_GIT=1 npm run release:npm   # same without git moves (CI-friendly)

License

MIT

About

Composable Vue directives for real UI behavior — no boilerplate, no framework friction.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors