Skip to content

Commit

Permalink
feat: class utils extended
Browse files Browse the repository at this point in the history
CSSUtils
- locks support - ability to control class modification source
- reverse class support - ability to negate modification
  • Loading branch information
ala-n committed Apr 18, 2021
1 parent 4e4b859 commit c3e7f66
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 20 deletions.
17 changes: 17 additions & 0 deletions pages/views/pages/basic/esl-toggleable.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ <h4>Hover event</h4>
</div>
</section>
<hr/>

<section class="row">
<div class="col-12">
<h4>Class toggle</h4>
<div style="height: 70px;">
<esl-popup id="popup3" class="d-block alert" active-class="alert-success !alert-danger">
<strong>Toggleable can be used to switch state.</strong> You can toggle it using the trigger bellow.
</esl-popup>
</div>
<p>Triggers</p>
<div>
<esl-trigger target="#popup3" class="btn btn-primary">Toggle type</esl-trigger>
</div>
</div>
</section>
<hr/>

<section class="row">
<div class="col-12">
<h4>Close On Body Click</h4>
Expand Down
4 changes: 2 additions & 2 deletions src/modules/esl-toggleable/core/esl-toggleable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class ESLToggleable extends ESLBaseElement {
activators.set(this, params.activator);
this.open = this._open = true;
CSSUtil.addCls(this, this.activeClass);
BodyClassManager.addCls(this.bodyClass, this);
CSSUtil.addCls(document.body, this.bodyClass, this);
this.updateA11y();
this.$$fire('esl:refresh');
}
Expand All @@ -221,7 +221,7 @@ export class ESLToggleable extends ESLBaseElement {
activators.delete(this);
this.open = this._open = false;
CSSUtil.removeCls(this, this.activeClass);
BodyClassManager.removeCls(this.bodyClass, this);
CSSUtil.removeCls(document.body, this.bodyClass, this);
this.updateA11y();
}

Expand Down
47 changes: 39 additions & 8 deletions src/modules/esl-utils/dom/styles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@

type Locks = Map<string, HTMLElement[]>;
const lockStore = new WeakMap<HTMLElement, Locks>();

const lock = (el: HTMLElement, className: string, locker: HTMLElement) => {
const locks = lockStore.get(el) || new Map();
const currentList = locks.get(className) || [];
currentList.push(locker);
locks.set(className, currentList);
lockStore.set(el, locks);
};
const unlock = (el: HTMLElement, className: string, locker: HTMLElement) => {
const locks = lockStore.get(el);
if (!locks) return true;
const currentList = locks.get(className) || [];
const lockIndex = currentList.indexOf(locker);
(lockIndex >= 0) && currentList.splice(lockIndex, 1);
return !currentList.length;
};

/** CSS manipulation utilities. */
export abstract class CSSUtil {
/** Splitting passed token string into CSS class names array. */
Expand All @@ -9,25 +29,36 @@ export abstract class CSSUtil {
* Add all classes from the class string to the element.
* Class string can be nullable or contain multiple classes separated by space.
* */
public static addCls(el: HTMLElement, cls: string | null | undefined) {
const tokens = CSSUtil.splitTokens(cls);
tokens.length && el.classList.add(...tokens);
public static addCls(el: HTMLElement, cls: string | null | undefined, locker?: HTMLElement) {
CSSUtil.splitTokens(cls).forEach((className) => CSSUtil.addOne(el, className, locker));
}

/**
* Remove all classes from the class string to the element.
* Class string can be nullable or contain multiple classes separated by space.
* */
public static removeCls(el: HTMLElement, cls: string | null | undefined) {
const tokens = CSSUtil.splitTokens(cls);
tokens.length && el.classList.remove(...tokens);
public static removeCls(el: HTMLElement, cls: string | null | undefined, locker?: HTMLElement) {
CSSUtil.splitTokens(cls).forEach((className) => CSSUtil.removeOne(el, className, locker));
}

/**
* Toggle all classes from the class string on the element to the passed state.
* Class string can be nullable or contain multiple classes separated by space.
* */
public static toggleClsTo(el: HTMLElement, cls: string | null | undefined, state: boolean) {
(state ? CSSUtil.addCls : CSSUtil.removeCls)(el, cls);
public static toggleClsTo(el: HTMLElement, cls: string | null | undefined, state: boolean, locker?: HTMLElement) {
(state ? CSSUtil.addCls : CSSUtil.removeCls)(el, cls, locker);
}

/** Add class to the element */
public static addOne(el: HTMLElement, className: string, locker?: HTMLElement): void {
if (className[0] === '!') return CSSUtil.removeOne(el, className.substr(1), locker);
if (locker) lock(el, className, locker);
el.classList.add(className);
}
/** Remove class from the element */
public static removeOne(el: HTMLElement, className: string, locker?: HTMLElement): void {
if (className[0] === '!') return CSSUtil.addOne(el, className.substr(1), locker);
if (locker && !unlock(el, className, locker)) return;
el.classList.remove(className);
}
}
73 changes: 63 additions & 10 deletions src/modules/esl-utils/dom/test/styles.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {CSSUtil} from '../styles';

describe('CSSUtil tests', () => {
describe('CSSUtil tests:', () => {
test('styles: crud simple', () => {
const el = document.createElement('div');

Expand Down Expand Up @@ -33,14 +33,67 @@ describe('CSSUtil tests', () => {
expect(el.classList.contains('a')).toBe(true);
});

test.each([
[''], [' '], [null], [undefined]
])('add %p safe check', (val) => {
const el = document.createElement('div');
expect(el.classList.length).toBe(0);
CSSUtil.addCls(el, val);
expect(el.classList.length).toBe(0);
CSSUtil.removeCls(el, val);
expect(el.classList.length).toBe(0);
describe('class locks:', () => {

const lock1 = document.createElement('div');
const lock2 = document.createElement('div');

test('lock case', () => {
const el = document.createElement('div');

CSSUtil.toggleClsTo(el, 'a', true, lock1);
expect(el.classList.contains('a')).toBeTruthy();
CSSUtil.toggleClsTo(el, 'a', true, lock2);
expect(el.classList.contains('a')).toBeTruthy();

CSSUtil.toggleClsTo(el, 'a', false, lock1);
expect(el.classList.contains('a')).toBeTruthy();
CSSUtil.toggleClsTo(el, 'a', false, lock2);
expect(el.classList.contains('a')).toBeFalsy();
});

const payloadSet = (new Array(1000)).fill('!a').join(' ');
test('payload test case', () => {
const el = document.createElement('div');
CSSUtil.removeCls(el, payloadSet, lock1);
expect(el.classList.contains('a')).toBeTruthy();
expect(el.classList.length).toBe(1);
CSSUtil.addCls(el, payloadSet, lock1);
expect(el.classList.contains('a')).toBeFalsy();
expect(el.classList.length).toBe(0);
}, 50);
});

describe('reverse adding:', () => {
test('add reverse', () => {
const el = document.createElement('div');
el.className = 'a b';
CSSUtil.addOne(el, '!a');
expect(el.classList.length).toBe(1);
CSSUtil.addOne(el, '!b');
expect(el.classList.length).toBe(0);
});
test('remove reverse', () => {
const el = document.createElement('div');
el.className = 'a b';
CSSUtil.removeOne(el, '!a');
expect(el.classList.contains('a')).toBeTruthy();
CSSUtil.removeOne(el, '!b');
expect(el.classList.contains('b')).toBeTruthy();
expect(el.classList.length).toBe(2);
});
});

describe('edge cases:', () => {
test.each([
[''], [' '], [null], [undefined]
])('%p safe check', (val) => {
const el = document.createElement('div');
expect(el.classList.length).toBe(0);
CSSUtil.addCls(el, val);
expect(el.classList.length).toBe(0);
CSSUtil.removeCls(el, val);
expect(el.classList.length).toBe(0);
});
});
});

0 comments on commit c3e7f66

Please sign in to comment.