Last active
January 14, 2020 09:23
-
-
Save fabd/8b5e4ea110842d2c1923e2e711d93f02 to your computer and use it in GitHub Desktop.
(TypeScript exercise) A small jquery-like utility library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A tiny jQuery inspired library for common DOM manipulation. | |
* | |
* BROWSER SUPPORT | |
* | |
* Recent versions of Edge, Chrome, Safari, iOS Safari, Chrome for Android (as per caniuse.com) | |
* NOT supported: IE <= 9, Opera Mini. | |
* | |
* | |
* NOTES | |
* | |
* Chaining a method is allowed *only once* after the constructor. To call more | |
* than one method, just assign the result (using `$` prefix as a convention | |
* to distinguish the actual element from the DomJS constructor): | |
* | |
* const $app = $('#app') | |
* $app.css('background', 'powderblue') | |
* $app.on('click', evt => { console.log('clicked %o', evt.target) }) | |
* | |
* | |
* EXAMPLES | |
* | |
* Import: | |
* | |
* import $ from 'dom' | |
* | |
* Use with selector (use `[0]` or `el()` to get the actual element): | |
* | |
* let $el = $('.box')[0] | |
* let $el = $('.box').el() | |
* | |
* Use with element: | |
* | |
* let parent = $('.box').closest('.container') => returns actual element | |
* $(parent).on(...) | |
* | |
* Convention: use a $ for instances of DomJS to distinguish from actual Element: | |
* | |
* let $box = Dom('.box') | |
* $box.css('width') = '20px' | |
* $box.on('click', (ev) => { console.log("clicked el %o", ev.target) }) | |
* | |
* | |
* METHODS | |
* | |
* el(i = 0) ... return the underlying DOM element (index defaults to 0) | |
* | |
* closest(selector) ... find the first ancestor matching selector | |
* | |
* css(prop) ... get/set inline styles | |
* css(prop, value) | |
* css({ prop1: value, ...}) | |
* | |
* each(callback) ... callback arguments: element, index (similar to | |
* forEach) | |
* | |
* on(event, callback) | |
* on([event1, event2 ...], fn) | |
* | |
* off(event) ... unbind event(s) or listener | |
* off([event1, event2 ...]) | |
* off(fn) | |
* | |
* once(event, callback) ... call handler once, then remove it | |
* | |
* remove(node) | |
* | |
* IMPORT METHODS | |
* | |
* getStyle() | |
* insertAfter(newNode, refNode) ... insert newNode as next sibling of refNode | |
* | |
*/ | |
import Lang from "./lang"; | |
// types | |
type StringOrStringArray = string | string[]; | |
type StringHash = { [key: string]: string }; | |
// aliases | |
const document = window.document; | |
const documentElement = document.documentElement; | |
// helpers | |
const inDocument = (el: Node | null) => documentElement.contains(el); | |
// | |
type $Event = { | |
el: DOMJS.ElementOrWindow; | |
type: string; | |
fn: EventListener; | |
}; | |
let $Events: $Event[] = []; | |
declare interface DomJSInterface { | |
/** | |
* Retrieve one of the elements matched by the selector. | |
* | |
* NOTE! Unlike jQuery's "get", el() by default returns the first | |
* element, for convenience: | |
* | |
* if (Dom('.box').el()) { | |
* // element is found... | |
* } | |
* | |
*/ | |
el(i?: number): DOMJS.ElementOrWindow; | |
/** | |
* Returns first ancestor matching selector, or null. | |
* | |
* NOTE! Unlike Element.closest() method, this function does NOT | |
* return the original element. | |
* | |
* const ul = $('.TodoList-item').closest('ul') as HTMLUListElement | |
*/ | |
closest(selector: string): Element | null; | |
/** | |
* Get / set inline style(s). | |
* | |
* Usage: | |
* | |
* css('prop') Get inline style | |
* css('prop', value) Set one inline style | |
* css({ prop1: value, ...}) Set multiple inline styles | |
* | |
*/ | |
css(props: string | object, value?: any): any; | |
/** | |
* Iterate over collection returned by the constructor. | |
* | |
* Return explicit `false` to end the loop. | |
* | |
* Example: | |
* | |
* $('#todolist li').each( (el, index) => { console.log(el, index) }) | |
*/ | |
each(callback: {(element: Element, index: number): false | void}): void; | |
/** | |
* Bind one or more event types to a listener, for a SINGLE element | |
* | |
* @param events One or more event types, eg. ['click', 'focus'] | |
* @param callback | |
*/ | |
on(events: string | string[], callback: EventListener): void; | |
/** | |
* Detach event listeners. | |
* | |
* @param events One or more event types, OR the listener to detach (one argument) | |
* @param callback (optional) Detach only events matching this callback | |
*/ | |
off(events: string | string[], callback?: EventListener): void; | |
/** | |
* Fire an event only once, then remove said event. | |
* | |
* Example: | |
* | |
* Dom(el).once('transitionend', fn) | |
* | |
*/ | |
once(event: string, fn: EventListener): void; | |
/** | |
* Removes the node from the tree it belongs to. | |
* | |
* @return Returns removed node, or null | |
*/ | |
remove(): Node | null; | |
} | |
type DomJSSelector = string | Window | Node; | |
export namespace DOMJS { | |
export type ElementOrWindow = Element | Window; | |
} | |
declare type Foo = string; | |
class DomJS implements DomJSInterface { | |
// ArrayLike | |
length: number; | |
[n: number]: DOMJS.ElementOrWindow; | |
constructor(selector: DomJSSelector, context: Element) { | |
let nodes: ArrayLike<DOMJS.ElementOrWindow>; | |
if (Lang.isString(selector)) { | |
nodes = (context || document).querySelectorAll(selector); | |
} | |
// window is not instanceof Node, has "length", but doesn't behave like an array | |
else if (Lang.isWindow(selector)) { | |
nodes = [window]; | |
} | |
// assume it's a Node | |
else { | |
this[0] = selector as Element; | |
this.length = 1; | |
return this; | |
} | |
for (let i = 0, l = (this.length = nodes.length); i < l; i++) { | |
this[i] = nodes[i]; | |
} | |
} | |
el(i?: number) { | |
return this[i || 0]; | |
} | |
closest(selector: string): Element | null { | |
console.assert(Lang.isString(selector), "closest() : selector is invalid"); | |
let el = this[0] as Element; | |
console.assert(Lang.isNode(el), "closest() : el is invalid"); | |
if (!inDocument(el)) { | |
return null; | |
} | |
let matchesSelector = (el: Element) => el.matches(selector); | |
while ((el = el.parentElement || (el.parentNode as Element))) { | |
if (matchesSelector(el)) return el | |
} | |
return null; | |
} | |
each(callback: {(element: Element, index: number): false | void}): void { | |
for (let i = 0, l = this.length; i < l; i++) { | |
if (false === callback(this[i] as Element, i)) break; | |
} | |
} | |
on(events: string | string[], callback: EventListener) { | |
let el = this[0]; | |
console.assert(el === window || Lang.isNode(el), "on() el is invalid"); | |
if (Lang.isString(events)) { | |
events = [events]; | |
} | |
events.forEach(event => { | |
el.addEventListener(event, callback, false); | |
$Events.push({ el: el, type: event, fn: callback }); | |
}); | |
} | |
off(events: string | string[] | EventListener | null) { | |
let callback: EventListener; | |
const el = this[0]; | |
console.assert(el === window || Lang.isNode(el), "off() : el is invalid"); | |
// .off('click') | |
if (Lang.isString(events)) { | |
events = [events]; | |
} | |
// .off(callback) | |
else if (Lang.isFunction(events)) { | |
callback = events as EventListener; | |
events = null; | |
} | |
// .off() | |
else { | |
console.assert(arguments.length === 0, "off(): invalid arguments"); | |
} | |
$Events = $Events.filter(e => { | |
if ( | |
e.el === el && | |
(!callback || callback === e.fn) && | |
(!events || (events as string[]).indexOf(e.type) > -1) | |
) { | |
e.el.removeEventListener(e.type, e.fn); | |
return false; | |
} | |
return true; | |
}); | |
} | |
once(event: string, fn: EventListener) { | |
let that = this; | |
let el = this[0]; | |
console.assert(Lang.isFunction(fn), "once() : fn is not a function"); | |
console.assert(el === window || Lang.isNode(el), "once() : el is invalid"); | |
let listener = function(this: any) { | |
// console.log('called once just now'); | |
(fn as Function).apply(this, arguments); | |
that.off(listener); | |
}; | |
this.on(event, listener); | |
} | |
css(props: string | StringHash, value?: any): any { | |
let element = this[0] as HTMLElement; | |
let styles: StringHash; | |
if (Lang.isString(props)) { | |
let prop = props; | |
// css('prop') | |
if (arguments.length === 1) { | |
console.assert(prop in element.style, "invalid property name"); | |
return element.style.getPropertyValue(prop); | |
} | |
// css('prop', value) | |
console.assert(!Lang.isUndefined(arguments[1])); | |
styles = { [prop]: value }; | |
} else { | |
styles = props; | |
} | |
// set one or more styles | |
for (let prop in styles) { | |
element.style.setProperty(prop, styles[prop]); | |
} | |
} | |
remove(): Node | null { | |
const node = this[0] as Node; | |
return (node.parentNode && node.parentNode.removeChild(node)) || null; | |
} | |
} | |
const factory = (selector: DomJSSelector, context?: any) => { | |
return new DomJS(selector, context); | |
}; | |
/** | |
* USAGE | |
* | |
* add(el, 'foo') | |
* add(el, 'foo bar') | |
* add(el, ['foo', 'bar']) | |
* | |
* remove(el, 'foo') | |
* remove(el, 'foo bar') | |
* remove(el, ['foo', 'bar']) | |
* | |
* toggle(el, name) | |
* toggle(el, name, force) | |
* | |
* | |
* COMPATIBILITY | |
* | |
* - IE10/11 : does not support add/remove of multiple classes (only the 1st one) | |
* | |
*/ | |
export const classList = { | |
_set(el: Node, names: StringOrStringArray, add: boolean) { | |
console.assert(Lang.isNode(el), "classList.add/remove : invalid node"); | |
console.assert( | |
Lang.isString(names) || Lang.isArray(names), | |
"classList : class must be a String or Array" | |
); | |
if (!el) return; | |
let classes: string[] = Lang.isString(names) | |
? names.split(" ") | |
: /* assumed Array */ names; | |
// FIXME? IE10/11 does not support multiple classes for add/remove (loop?) | |
(el as HTMLElement).classList[add ? "add" : "remove"](...classes); | |
}, | |
add(el: Node, names: StringOrStringArray) { | |
this._set(el, names, true); | |
}, | |
remove(el: Node, names: StringOrStringArray) { | |
this._set(el, names, false); | |
}, | |
toggle(el: Node, name: string, force: boolean) { | |
// NOTE: doing it this way supports IE10/11 lack of support for "force" | |
if (arguments.length > 2) { | |
this._set(el, [name], !!force); | |
} else { | |
(el as HTMLElement).classList.toggle(name); | |
} | |
} | |
}; | |
export { factory as default }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// language utils | |
const isNode = (el: any) => { | |
return el instanceof Node; | |
}; | |
const isNodeList = (el: any) => { | |
return ( | |
el instanceof NodeList || | |
el instanceof HTMLCollection || | |
el instanceof Array | |
); | |
}; | |
const isArray = (o: any): boolean => Array.isArray(o); | |
const isBoolean = (o: any): o is boolean => typeof o === "boolean"; | |
const isFunction = (f: any): f is Function => typeof f === "function"; | |
const isNumber = (s: any): s is number => typeof s === "number"; | |
const isString = (s: any): s is string => typeof s === "string"; | |
const isUndefined = (o: any): o is undefined => typeof o === "undefined"; | |
const isWindow = (o: any): o is Window => o === window; | |
export default { | |
isArray, | |
isBoolean, | |
isFunction, | |
isNode, | |
isNodeList, | |
isNumber, | |
isString, | |
isUndefined, | |
isWindow | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment