function buildExtension(extensionClass, options) {
const { defaults } = extensionClass;
const { autoBind, autoListen, autoSelect, compactOptions } = options;
const optionGroupNames = ['classNames', 'selectors'].filter(name => name in defaults);
Object.assign(extensionClass, {
extend(subject, options = {}) {
let { element, elements } = _parseSubject(subject, options);
let { contextElement: context } = options;
let root = (context || element);
root.classList.add(this.className());
options = _assignOptions(options,
defaults, optionGroupNames, root.getAttribute(this.attrName())
);
let instance = new this(
element || elements, Object.assign({}, options), context
);
instance._setUpCleanupTasks();
Object.assign(instance, compactOptions ? options : { options });
Object.assign(instance, {
element, elements, contextElement: context, rootElement: root,
});
if (autoBind) {
_bindMethods(instance, { properties: this.prototype });
}
if (autoListen) {
_listen(instance);
}
if (autoSelect) {
instance.selectToProperties();
}
if (instance.init) {
instance.init();
}
return instance;
},
});
_mix(extensionClass, options, optionGroupNames);
if (extensionClass.init) {
extensionClass.init();
}
}
function _assignOptions(options, defaults, groupNames, attribute) {
if (attribute) {
try { Object.assign(options, JSON.parse(attribute)); } catch (error) {}
}
options = Object.assign({}, defaults, options);
groupNames.forEach((g) => options[g] = Object.assign({}, defaults[g], options[g]));
return options;
}
function _bindMethods(object, { context, properties }) {
Object.getOwnPropertyNames(properties || object)
.filter(name => typeof object[name] === 'function' && name !== 'constructor')
.forEach(name => object[name] = object[name].bind(context || object));
}
function _listen(instance) {
if (!instance.addEventListeners || !instance.eventListeners) {
throw 'Missing requirements.';
}
const { eventListeners } = instance;
_bindMethods(eventListeners, { context: instance });
instance.addEventListeners(eventListeners);
instance._cleanupTasks.push(() => {
instance.removeEventListeners(eventListeners);
});
if (instance._onWindowResize && instance.resizeDelay) {
let ran, { _onWindowResize } = instance;
instance._onWindowResize = function(event) {
if (ran && Date.now() < ran + this.resizeDelay) { return; }
ran = Date.now();
_onWindowResize.call(this, event);
}.bind(instance);
window.addEventListener('resize', instance._onWindowResize);
instance._cleanupTasks.push(() => {
window.removeEventListener('resize', instance._onWindowResize);
});
}
}
const _mixins = {};
_mixins.css = {
cssDuration(name, element) {
if (!element) { element = this.rootElement; }
return 1000 * parseFloat(getComputedStyle(element)[name]);
},
cssVariable(name, element) {
if (!element) { element = this.rootElement; }
return getComputedStyle(element).getPropertyValue(this.varName(name));
},
cssVariableDuration(name, element) {
return 1000 * parseFloat(this.cssVariable(name, element));
},
swapClasses(nameFrom, nameTo, element) {
if (!element) { element = this.rootElement; }
element.classList.remove(this.className(nameFrom));
element.classList.add(this.className(nameTo));
},
};
_mixins.debug = (debug, toPrefix) => (debug ? {
debugLog(...args) {
if (!this._hasDebugLogGroup) {
args.unshift(toPrefix('log'));
}
HLF.debugLog(...args);
},
debugLogGroup(arg) {
if (arg === false) {
console.groupEnd();
this._hasDebugLogGroup = false;
} else {
let args = [toPrefix('log')];
if (arg) { args.push(arg); }
console.group(...args);
this._hasDebugLogGroup = true;
}
},
} : {
debugLog() {},
debugLogGroup() {},
});
_mixins.event = {
addEventListeners(info, target) {
target = target || this.rootElement;
_normalizeEventListenersInfo(info);
Object.keys(info).forEach((type) => {
const [handler, options] = info[type];
target.addEventListener(type, handler, options);
});
},
removeEventListeners(info, target) {
target = target || this.rootElement;
_normalizeEventListenersInfo(info);
Object.keys(info).forEach((type) => {
const [handler, options] = info[type];
target.removeEventListener(type, handler, options);
});
},
toggleEventListeners(on, info, target) {
this[`${on ? 'add' : 'remove'}EventListeners`](info, target);
},
createCustomEvent(type, detail) {
let initArgs = { detail };
initArgs.bubbles = true;
return new CustomEvent(this.eventName(type), initArgs);
},
dispatchCustomEvent(type, detail = {}) {
return this.rootElement.dispatchEvent(this.createCustomEvent(type, detail));
},
};
_mixins.naming = (toPrefix) => ({
attrName(name = '') {
if (name.length) {
name = `-${name}`;
}
return `data-${toPrefix('data')}${name}`;
},
className(name = '') {
if (name.length) {
name = `-${name}`;
}
return `js-${toPrefix('class')}${name}`;
},
eventName(name) {
return `${toPrefix('event')}${name}`;
},
varName(name) {
return `--${toPrefix('var')}-${name}`;
},
});
_mixins.options = (defaults, groupNames) => ({
configure(options) {
Object.keys(options).forEach((name) => {
if (name in this || (this.options && name in this.options)) { return; }
delete options[name];
throw 'Not an existing option.';
});
let store = this.options || this;
Object.keys(options).filter(name => options[name] === 'default').forEach((name) => {
options[name] = defaults[name];
delete store[name];
});
groupNames.forEach((name) => {
store[name] = Object.assign({}, store[name], options[name]);
delete options[name];
});
Object.assign(store, options);
},
});
_mixins.remove = {
remove() {
this._cleanupTasks.forEach(task => task(this));
if (this.deinit) {
this.deinit();
}
},
_setUpCleanupTasks() {
this._cleanupTasks = [];
},
};
_mixins.selection = {
selectByClass(name, element) {
if (!element) { element = this.rootElement; }
return element.querySelector(`.${this.className(name)}`);
},
selectAllByClass(name, element) {
if (!element) { element = this.rootElement; }
return element.querySelectorAll(`.${this.className(name)}`);
},
selectToProperties() {
const selectors = this.options ? this.options.selectors : this.selectors;
if (!this.rootElement || !selectors) {
throw 'Missing requirements.';
}
Object.keys(selectors).forEach((name) => {
const selector = selectors[name];
if (name.substr(-1) === 's') {
this[name] = this.rootElement.querySelectorAll(selector);
} else {
this[name] = this.rootElement.querySelector(selector);
}
});
},
};
_mixins.timing = {
setElementTimeout(element, name, duration, callback) {
name = this.attrName(name);
if (element.getAttribute(name)) {
clearTimeout(element.getAttribute(name));
}
let timeout = null;
if (duration != null && callback) {
timeout = setTimeout(() => {
callback();
element.removeAttribute(name);
}, duration);
}
if (timeout) {
element.setAttribute(name, timeout);
} else {
element.removeAttribute(name);
}
},
setTimeout(name, duration, callback) {
if (this[name]) {
clearTimeout(this[name]);
}
let timeout = null;
if (duration != null && callback) {
timeout = setTimeout(() => {
callback();
this[name] = null;
}, duration);
}
this[name] = timeout;
},
};
function _mix(extensionClass, options, optionGroupNames) {
const { debug, defaults, toPrefix } = extensionClass;
Object.assign(extensionClass, _mixins.naming(toPrefix));
let { autoListen, autoSelect, mixinNames: names } = options, flags = {};
Object.keys(_mixins).forEach(n => flags[n] = false);
(names || []).concat('debug', 'naming', 'options', 'remove', 'timing')
.forEach(n => flags[n] = true);
if (autoListen) { flags.event = true; }
if (autoSelect) { flags.selection = true; }
names = Object.keys(flags).filter(n => flags[n]);
Object.assign(extensionClass.prototype, ...names.map((name) => {
let mixin = _mixins[name];
if (typeof mixin === 'function') {
if (name === 'debug') { mixin = mixin(debug, toPrefix); }
else if (name === 'naming') { mixin = mixin(toPrefix); }
else if (name === 'options') { mixin = mixin(defaults, optionGroupNames); }
else { mixin = mixin(); }
}
return mixin;
}));
}
function _normalizeEventListenersInfo(info) {
Object.keys(info).forEach((type) => {
if (typeof info[type] !== 'function') { return; }
info[type] = [info[type]];
});
}
function _parseSubject(subject, options) {
let element, elements;
if (subject instanceof HTMLElement) {
element = subject;
} else if (typeof subject === 'function') {
Object.assign(options, { querySelector: subject });
return _parseSubject(subject(options.contextElement), options);
} else {
elements = Array.from(subject);
}
return { element, elements };
}
Object.assign(HLF, { buildExtension });
if (HLF.debug && typeof window === 'object') {
Object.assign(window, { HLF });
}
return HLF;
});
HLF Extensions Core
Tests
The extensions core provides shared functionality for extension classes to reduce boilerplate around common tasks. Static methods like
extend
and helpers as added, and instance methods are mixed onto the prototype.