UNPKG

@v4fire/client

Version:

V4Fire client core library

610 lines (517 loc) • 13.9 kB
/*! * V4Fire Client Core * https://github.com/V4Fire/Client * * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ /** * [[include:super/i-block/modules/block/README.md]] * @packageDocumentation */ import type iBlock from 'super/i-block/i-block'; import { fakeCtx, modRgxpCache, elRxp } from 'super/i-block/modules/block/const'; import Friend from 'super/i-block/modules/friend'; import type { ModsTable, ModsNTable } from 'super/i-block/modules/mods'; import type { ModEvent, ModEventReason, SetModEvent, ElementModEvent, SetElementModEvent } from 'super/i-block/modules/block/interface'; export * from 'super/i-block/modules/block/interface'; /** * Class implements BEM-like API */ export default class Block extends Friend { /** * Map of applied modifiers */ protected readonly mods?: Dictionary<CanUndef<string>>; constructor(component: iBlock) { super(component); this.mods = Object.createDict(); for (let m = component.mods, keys = Object.keys(m), i = 0; i < keys.length; i++) { const name = keys[i]; this.setMod(name, m[name], 'initSetMod'); } } /** * Returns a full name of the current block * * @param [modName] * @param [modValue] * * @example * ```js * // b-foo * this.getFullBlockName(); * * // b-foo_focused_true * this.getFullBlockName('focused', true); * ``` */ getFullBlockName(modName?: string, modValue?: unknown): string { return this.componentName + (modName != null ? `_${modName.dasherize()}_${String(modValue).dasherize()}` : ''); } /** * Returns a CSS selector to the current block * * @param [mods] - additional modifiers * * @example * ```js * // .b-foo * this.getBlockSelector(); * * // .b-foo.b-foo_focused_true * this.getBlockSelector({focused: true}); * ``` */ getBlockSelector(mods?: ModsTable): string { let res = `.${this.getFullBlockName()}`; if (mods) { for (let keys = Object.keys(mods), i = 0; i < keys.length; i++) { const key = keys[i]; res += `.${this.getFullBlockName(key, mods[key])}`; } } return res; } /** * Returns a full name of the specified element * * @param name - element name * @param [modName] * @param [modValue] * * @example * ```js * // b-foo__bla * this.getFullElName('bla'); * * // b-foo__bla_focused_true * this.getBlockSelector('bla', 'focused', true); * ``` */ getFullElName(name: string, modName?: string, modValue?: unknown): string { const modStr = modName != null ? `_${modName.dasherize()}_${String(modValue).dasherize()}` : ''; return `${this.componentName}__${name.dasherize()}${modStr}`; } /** * Returns a CSS selector to the specified element * * @param name - element name * @param [mods] - additional modifiers * * @example * ```js * // .$componentId.b-foo__bla * this.getElSelector('bla'); * * // .$componentId.b-foo__bla.b-foo__bla_focused_true * this.getElSelector('bla', {focused: true}); * ``` */ getElSelector(name: string, mods?: ModsTable): string { let res = `.${this.componentId}.${this.getFullElName(name)}`; if (mods) { for (let keys = Object.keys(mods), i = 0; i < keys.length; i++) { const key = keys[i]; res += `.${this.getFullElName(name, key, mods[key])}`; } } return res; } /** * Returns block child elements by the specified request. * This overload is used to optimize DOM searching. * * @param ctx - context node * @param name - element name * @param [mods] - additional modifiers * * @example * ```js * this.elements(node, 'foo'); * this.elements(node, 'foo', {focused: true}); * ``` */ elements<E extends Element = Element>(ctx: Element, name: string, mods?: ModsTable): NodeListOf<E>; /** * Returns block child elements by the specified request * * @param name - element name * @param [mods] - additional modifiers * * @example * ```js * this.elements('foo'); * this.elements('foo', {focused: true}); * ``` */ elements<E extends Element = Element>(name: string, mods?: ModsTable): NodeListOf<E>; elements<E extends Element = Element>( ctxOrName: Element | string, name?: string | ModsTable, mods?: ModsTable ): NodeListOf<E> { let ctx = this.node, elName; if (Object.isString(ctxOrName)) { elName = ctxOrName; if (Object.isPlainObject(name)) { mods = name; } } else { elName = name; ctx = ctxOrName; } ctx ??= this.node; if (ctx == null) { return fakeCtx.querySelectorAll('.loopback'); } return ctx.querySelectorAll(this.getElSelector(elName, mods)); } /** * Returns a block child element by the specified request. * This overload is used to optimize DOM searching. * * @param ctx - context node * @param name - element name * @param [mods] - map of additional modifiers * * @example * ```js * this.element(node, 'foo'); * this.element(node, 'foo', {focused: true}); * ``` */ element<E extends Element = Element>(ctx: Element, name: string, mods?: ModsTable): CanUndef<E>; /** * Returns a block child element by the specified request * * @param name - element name * @param [mods] - additional modifiers * * @example * ```js * this.element('foo'); * this.element('foo', {focused: true}); * ``` */ element<E extends Element = Element>(name: string, mods?: ModsTable): CanUndef<E>; element<E extends Element = Element>( ctxOrName: Element | string, name?: string | ModsTable, mods?: ModsTable ): CanUndef<E> { let ctx = this.node, elName; if (Object.isString(ctxOrName)) { elName = ctxOrName; if (Object.isPlainObject(name)) { mods = name; } } else { elName = name; ctx = ctxOrName; } ctx ??= this.node; if (ctx == null) { return undefined; } return ctx.querySelector<E>(this.getElSelector(elName, mods)) ?? undefined; } /** * Sets a modifier to the current block. * The method returns false if the modifier is already set. * * @param name - modifier name * @param value * @param [reason] - reason to set a modifier * * @example * ```js * this.setMod('focused', true); * this.setMod('focused', true, 'removeMod'); * ``` */ setMod(name: string, value: unknown, reason: ModEventReason = 'setMod'): boolean { if (value == null) { return false; } name = name.camelize(false); const {mods, node, ctx} = this; const normalizedVal = String(value).dasherize(), prevVal = this.getMod(name); if (prevVal === normalizedVal) { return false; } const isInit = reason === 'initSetMod'; let prevValFromDOM, needSync = false; if (isInit) { prevValFromDOM = this.getMod(name, true); needSync = prevValFromDOM !== normalizedVal; } if (needSync) { this.removeMod(name, prevValFromDOM, 'initSetMod'); } else if (!isInit) { this.removeMod(name, undefined, 'setMod'); } if (node != null && (!isInit || needSync)) { node.classList.add(this.getFullBlockName(name, normalizedVal)); } if (mods != null) { mods[name] = normalizedVal; } ctx.mods[name] = normalizedVal; if (ctx.isNotRegular === false) { const // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition watchModsStore = ctx.field?.get<ModsNTable>('watchModsStore'); if (watchModsStore != null && name in watchModsStore && watchModsStore[name] !== normalizedVal) { delete Object.getPrototypeOf(watchModsStore)[name]; ctx.field.set(`watchModsStore.${name}`, normalizedVal); } } if (!isInit || !ctx.isFlyweight) { const event = <SetModEvent>{ event: 'block.mod.set', type: 'set', name, value: normalizedVal, prev: prevVal, reason }; this.localEmitter .emit(`block.mod.set.${name}.${normalizedVal}`, event); if (!isInit) { // @deprecated ctx.emit(`mod-set-${name}-${normalizedVal}`, event); ctx.emit(`mod:set:${name}:${normalizedVal}`, event); } } return true; } /** * Removes a modifier from the current block. * The method returns false if the block does not have this modifier. * * @param name - modifier name * @param [value] * @param [reason] - reason to remove a modifier * * @example * ```js * this.removeMod('focused'); * this.removeMod('focused', true); * this.removeMod('focused', true, 'setMod'); * ``` */ removeMod(name: string, value?: unknown, reason: ModEventReason = 'removeMod'): boolean { name = name.camelize(false); value = value != null ? String(value).dasherize() : undefined; const {mods, node, ctx} = this; const isInit = reason === 'initSetMod', currentVal = this.getMod(name, isInit); if (currentVal === undefined || value !== undefined && currentVal !== value) { return false; } if (node != null) { node.classList.remove(this.getFullBlockName(name, currentVal)); } if (mods != null) { mods[name] = undefined; } const needNotify = reason === 'removeMod'; if (needNotify) { ctx.mods[name] = undefined; if (ctx.isNotRegular === false) { const // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition watchModsStore = ctx.field?.get<ModsNTable>('watchModsStore'); if (watchModsStore != null && name in watchModsStore && watchModsStore[name] != null) { delete Object.getPrototypeOf(watchModsStore)[name]; ctx.field.set(`watchModsStore.${name}`, undefined); } } } if (!isInit || !ctx.isFlyweight) { const event = <ModEvent>{ event: 'block.mod.remove', type: 'remove', name, value: currentVal, reason }; this.localEmitter .emit(`block.mod.remove.${name}.${currentVal}`, event); if (needNotify) { // @deprecated ctx.emit(`mod-remove-${name}-${currentVal}`, event); ctx.emit(`mod:remove:${name}:${currentVal}`, event); } } return true; } /** * Returns a value of the specified block modifier * * @param name - modifier name * @param [fromNode] - if true, then the modifier value will be taken from a DOM node * * @example * ```js * this.getMod('focused'); * this.getMod('focused', true); * ``` */ getMod(name: string, fromNode?: boolean): CanUndef<string> { const {mods, node} = this; if (mods != null && !fromNode) { return mods[name.camelize(false)]; } if (node == null) { return undefined; } const MOD_VALUE = 2; const pattern = `(?:^| )(${this.getFullBlockName(name, '')}[^_ ]*)`, modRgxp = modRgxpCache[pattern] ?? new RegExp(pattern), el = modRgxp.exec(node.className); modRgxpCache[pattern] = modRgxp; return el ? el[1].split('_')[MOD_VALUE] : undefined; } /** * Sets a modifier to the specified element. * The method returns false if the modifier is already set. * * @param link - link to the element * @param elName - element name * @param modName * @param value * @param [reason] - reason to set a modifier * * @example * ```js * this.setElMod(node, 'foo', 'focused', true); * this.setElMod(node, 'foo', 'focused', true, 'initSetMod'); * ``` */ setElMod( link: Nullable<Element>, elName: string, modName: string, value: unknown, reason: ModEventReason = 'setMod' ): boolean { if (!link || value == null) { return false; } elName = elName.camelize(false); modName = modName.camelize(false); const normalizedVal = String(value).dasherize(); if (this.getElMod(link, elName, modName) === normalizedVal) { return false; } this.removeElMod(link, elName, modName, undefined, 'setMod'); link.classList.add(this.getFullElName(elName, modName, normalizedVal)); const event = <SetElementModEvent>{ element: elName, event: 'el.mod.set', type: 'set', link, modName, value: normalizedVal, reason }; this.localEmitter.emit(`el.mod.set.${elName}.${modName}.${normalizedVal}`, event); return true; } /** * Removes a modifier from the specified element. * The method returns false if the element does not have this modifier. * * @param link - link to the element * @param elName - element name * @param modName * @param [value] * @param [reason] - reason to remove a modifier * * @example * ```js * this.removeElMod(node, 'foo', 'focused'); * this.removeElMod(node, 'foo', 'focused', true); * this.removeElMod(node, 'foo', 'focused', true, 'setMod'); * ``` */ removeElMod( link: Nullable<Element>, elName: string, modName: string, value?: unknown, reason: ModEventReason = 'removeMod' ): boolean { if (!link) { return false; } elName = elName.camelize(false); modName = modName.camelize(false); const normalizedVal = value != null ? String(value).dasherize() : undefined, currentVal = this.getElMod(link, elName, modName); if (currentVal === undefined || normalizedVal !== undefined && currentVal !== normalizedVal) { return false; } link.classList .remove(this.getFullElName(elName, modName, currentVal)); const event = <ElementModEvent>{ element: elName, event: 'el.mod.remove', type: 'remove', link, modName, value: currentVal, reason }; this.localEmitter.emit(`el.mod.remove.${elName}.${modName}.${currentVal}`, event); return true; } /** * Returns a value of a modifier from the specified element * * @param link - link to the element * @param elName - element name * @param modName - modifier name */ getElMod(link: Nullable<Element>, elName: string, modName: string): CanUndef<string> { if (!link) { return undefined; } const MOD_VALUE = 3; const pattern = `(?:^| )(${this.getFullElName(elName, modName, '')}[^_ ]*)`, modRgxp = pattern[pattern] ?? new RegExp(pattern), el = modRgxp.exec(link.className); modRgxpCache[pattern] = modRgxp; return el != null ? el[1].split(elRxp)[MOD_VALUE] : undefined; } }