@oruga-ui/oruga-next
Version:
UI components for Vue.js and CSS framework agnostic
283 lines (247 loc) • 8.51 kB
text/typescript
import {
hasInjectionContext,
inject,
type App,
type InjectionKey,
type ObjectPlugin,
} from "vue";
import { getValueByPath, merge, setValueByPath } from "./helpers";
import { isClient } from "./ssr";
import { addProgrammatic } from "./plugins";
import type { DeepKeys, DeepType, OrugaConfig } from "@/types";
// extend the OrugaProgrammatic interface with `config` programmatic interface
declare module "../index" {
interface OrugaProgrammatic {
config: typeof ConfigProgrammatic;
}
}
/** Interface of the programmtic component. interfaces. */
export interface OrugaProgrammatic {
[key: string]: Record<string, any>;
}
export type OrugaComponentPlugin = ObjectPlugin<{ oruga: Oruga }>;
/** Oruga Vue Plugin Object */
export interface Oruga {
install(app: App, options?: OrugaConfig): void;
/**
* The vue instance oruga got registered into.
* @internal
*/
_app: App;
/**
* The global config object for this instance.
* @internal
*/
_config: OrugaConfig;
/**
* All registered components programmatic interfaces.
* @internal
*/
_programmatic: OrugaProgrammatic;
/**
* Defines if this oruga instance is a testing instance.
* @internal
*/
_testing?: boolean;
/**
* Adds a Oruga component plugin.
*
* @param plugin - Oruga component plugin to add
* @returns Oruga plugin instance
*/
use(plugin: OrugaComponentPlugin): Oruga;
use(...plugins: OrugaComponentPlugin[]): Oruga;
/**
* Provides the oruga instance to the app context.
* This is useful when you need to use `useOruga()` in a different app instance than the one Oruga got installed in.
* @param app - The app instance to provide the oruga instance to.
*/
provide(app: App): void;
}
/** provide/inject oruga key symbol */
const $ORUGA_SYMBOL = Symbol("$oruga") as InjectionKey<Oruga>;
/**
* The global oruga instance which allows colling `useOruga()` outside of a component setup
* after the oruga plugin got installed.
*/
let globalOruga: Oruga | undefined;
export function setActiveOruga(oruga: Oruga | undefined): void {
globalOruga = oruga;
}
/**
* Get the currently active oruga instance if there is any.
*/
export function getActiveOruga(): Oruga | undefined {
return (hasInjectionContext() && inject($ORUGA_SYMBOL)) || globalOruga;
}
/**
* Create a new Oruga Vue plugin which sets the oruga config options.
* @param config - Override or extend the default oruga config.
* @param plugins - A list of component plugins to register them globally. By default, no components will be registered globally.
* @returns A new Oruga instance.
*/
export function createOruga(
config: OrugaConfig = {},
plugins: OrugaComponentPlugin[] = [],
): Oruga {
const _plugins: OrugaComponentPlugin[] = [];
const oruga: Oruga = {
// the vue instance Oruga gets installed in, it will be set in the install function
// @ts-expect-error it's actually empty here
_app: null,
// default config is defined here
_config: {
override: false,
iconPack: "mdi",
useHtml5Validation: true,
statusIcon: true,
transformClasses: undefined,
mobileBreakpoint: "1023px",
teleportTarget: () => (isClient ? document.body : "body"),
...config,
},
// map of all registered programmatic interfaces
// @ts-expect-error it's empty at first
_programmatic: {},
// vue plugin install function
install(app: App, options: OrugaConfig = {}): void {
// set global vue instance for programmatic usage
oruga._app = app;
// merge additional options with the base config
oruga._config = merge(oruga._config, options, true);
// set the global oruga instance
setActiveOruga(oruga);
// provide the oruga instance to the app context
this.provide(app);
// register the programmatic config interface to the programmatic oruga object
addProgrammatic(oruga, "config", ConfigProgrammatic);
// register oruga component plugins
_plugins.forEach((p) => app.use(p, { oruga }));
},
// helper to register component plugins on install
use(...plugins: OrugaComponentPlugin[]): Oruga {
plugins.forEach((p) => _plugins.push(p));
return oruga;
},
// helper to provide the oruga instance to another app instance
provide(app: App): void {
// provide the oruga instance
app.provide($ORUGA_SYMBOL, oruga);
// set the oruga instance as globalProperties
app.config.globalProperties.$oruga = oruga;
},
};
// register global components
oruga.use(...plugins);
return oruga;
}
/**
* Create a new oruga instance and set it as active even the plugin got not installed yet.
* @param config - Override or extend the default oruga config.
* @returns A new Oruga instance.
*/
export function createTestingOruga(config: OrugaConfig = {}): Oruga {
const oruga = createOruga(config);
oruga._testing = true;
setActiveOruga(oruga);
return oruga;
}
/**
* Get the oruga instence by injecting from the current context.
* Throws an error when no inject context or no oruga instance is available.
* @internal
*/
function getOruga(): Oruga {
// inject the current active oruga instance
const oruga = getActiveOruga();
if (!oruga)
throw new Error(
"No Oruga instance available. Have you installed the Oruga plugin yet?",
);
return oruga;
}
/**
* Composable for internal and external usage of programmatic components.
* It returns the programmatic interface of the current active oruga instance if there is any.
*/
export function useOruga(): OrugaProgrammatic {
return getActiveOruga()?._programmatic ?? ({} as OrugaProgrammatic);
}
/**
* Get the config of the current active oruga instance if there is any.
*/
export function getConfig(): OrugaConfig {
return getActiveOruga()?._config ?? {};
}
/**
* Override the config of the current active Oruga instance.
* The given config will be mergen in the existing one.
* Throws an error when no inject context or no oruga instance is available.
*
* @param config - The new config to be mergen into the existing one.
*/
export function setConfig(config: OrugaConfig): void {
const oruga = getOruga();
const _config = merge(oruga._config, config, true);
oruga._config = _config;
}
/**
* Get a config option by an option path
* with an optional default value as fallback when no config is set.
*/
export function getOption<
K extends DeepKeys<OrugaConfig>,
D extends DeepType<OrugaConfig, K>,
>(path: K | (string & {}), defaultValue: D): D;
export function getOption<
K extends DeepKeys<OrugaConfig>,
D extends DeepType<OrugaConfig, K>,
>(path: K | (string & {}), defaultValue?: D): D | undefined;
export function getOption<
K extends DeepKeys<OrugaConfig>,
D extends DeepType<OrugaConfig, K>,
>(path: K | (string & {}), defaultValue?: D): D | undefined {
const config = getConfig();
return getValueByPath<OrugaConfig, K, D>(config, path, defaultValue);
}
/** Add a config option to the config object. */
function setOption<K extends DeepKeys<OrugaConfig>>(
path: K,
value: DeepType<OrugaConfig, K>,
): void {
const oruga = getOruga();
setValueByPath(oruga._config, path, value);
}
/**
* Less type strict version of getOption for component props defaults.
* @internal
*/
export function getDefault<T>(
path: DeepKeys<OrugaConfig>,
defaultValue?: T,
): T {
const config = getConfig();
return getValueByPath(config, path, defaultValue) as T & {};
}
/**
* less type strict version of getOption for component prop default with type function
* @internal
*/
export function getDefaultFunction<T>(
path: DeepKeys<OrugaConfig>,
defaultValue?: T,
): (...args) => any {
return (...args) => {
const f = getDefault(path, defaultValue);
if (typeof f === "function") return f(...args);
};
}
const ConfigProgrammatic = {
getOption,
setOption,
getConfig,
setConfig,
};
export function useConfigProgrammatic(): typeof ConfigProgrammatic {
return ConfigProgrammatic;
}