New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@joinbox/overlay

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@joinbox/overlay - npm Package Compare versions

Comparing version 1.0.1 to 1.0.2

CHANGELOG.md

232

OverlayButtonElement.js

@@ -1,6 +0,228 @@

import OverlayButton from './OverlayButton.js';
(function () {
'use strict';
/* global window */
if (!window.customElements.get('overlay-button-component')) {
window.customElements.define('overlay-button-component', OverlayButton);
}
/**
* Gets/validates attribute of a HTML element.
* OUTDATED - use canReadAttributes instead.
* @param {HTMLElement} options.element
* @param {name} options.name Name of the attribute
* @param {function} options.validate Validate function; return true if attribute is valid
* @param {boolean} options.isSet True if you only want to know if the attribute is
* set (but do not care about its value).
* @param {string} errorMessage Additional error message
* @return {*} String if isSet is false, else boolean
*/
var getAndValidateAttribute = ({
element,
name,
validate = () => true,
isSet = false,
errorMessage = 'HTML attribute not valid',
} = {}) => {
if (!name) {
throw new Error(`getAndValidateAttribute: Pass an argument { name }; you passed ${name} instead.`);
}
/* global HTMLElement */
if (!element || !(element instanceof HTMLElement)) {
throw new Error(`getAndValidateAttribute: Pass an argument { element } that is a HTMLElement; you passed ${element} instead.`);
}
if (isSet) {
const hasAttribute = element.hasAttribute(name);
if (validate(hasAttribute) !== true) throw new Error(`getAndValidateAttribute: Attribute ${name} did not pass validation, is ${hasAttribute}: ${errorMessage}.`);
return hasAttribute;
}
// Do not use dataset as it's slower
// (https://calendar.perfplanet.com/2012/efficient-html5-data-attributes/) and provides
// less flexibility (in case we don't want the data- prefix)
const value = element.getAttribute(name);
if (validate(value) !== true) throw new Error(`getAndValidateAttribute: Attribute ${name} did not pass validation, is ${value}: ${errorMessage}.`);
return value;
};
/**
* Mixin for a component that announces itself by dispatching an event. If another element handles
* the event, it may pass the model to the current component by calling setModel(). The model will
* be stored in this.model. After model is set, this.onModelChange is called, if available.
* @example
* class extends HTMLElement {
* constructor() {
* Object.assign(this, canAnnounceElement);
* }
* async connectedCallback() {
* await this.announce();
* // Now this.model is ready
* this.model.on('change', this.update.bind(this));
* }
* }
*/
var canAnnounceElement = ({ eventName = 'announce-element', eventType, eventIdentifier } = {}) => {
// Create a (private) promise that is resolved when the model changes (see setModel). Do not
// define it within the object to not pollute its scope.
let resolveModelInitializedPromise;
const modelInitializedPromise = new Promise((resolve) => {
resolveModelInitializedPromise = resolve;
});
return {
model: undefined,
/**
* Dispatches announce event with a short delay; returns a promise that resolves after
* the event was dispatched.
*/
announce() {
/* global CustomEvent */
const event = new CustomEvent(eventName, {
bubbles: true,
detail: {
element: this,
eventType,
eventIdentifier,
},
});
// Short delay to make sure event listeners on parent elements (where the event bubbles
// to) are ready
setTimeout(() => {
this.dispatchEvent(event);
});
// Return promise that is resolved as soon as setModel is called for the first time
return modelInitializedPromise;
},
setModel(model) {
this.model = model;
resolveModelInitializedPromise();
},
};
};
/**
* Adds event listener to an element and returns removeEventListener function that only needs to
* be called to de-register an event.
* @example
* const disposer = createListener(window, 'click', () => {});
*/
var createListener = (element, eventName, handler) => {
// Takes this from execution context which must be the custom element
element.addEventListener(eventName, handler);
return () => element.removeEventListener(eventName, handler);
};
/* global HTMLElement */
/**
* Button that opens or closes or toggles an overlay. Requires
* attributes data-button-type (open/close/toggle) and data-overlay-name.
*/
class OverlayButton extends HTMLElement {
constructor() {
super();
this.readAttributes();
Object.assign(
this,
canAnnounceElement({ eventType: 'overlay-button', eventIdentifier: this.name }),
);
this.setupClickListener();
}
async connectedCallback() {
await this.announce();
this.handleModelChanges();
this.updateDOM();
}
readAttributes() {
this.name = this.getName();
this.type = this.getType();
const [openClass, closedClass] = this.getClassNames();
this.openClass = openClass;
this.closedClass = closedClass;
}
getClassNames() {
return [
getAndValidateAttribute({
element: this,
name: 'data-open-class-name',
}),
getAndValidateAttribute({
element: this,
name: 'data-closed-class-name',
}),
];
}
/**
* Reads overlay name from DOM, stores it in this.name
* @private
*/
getName() {
return getAndValidateAttribute({
element: this,
name: 'data-overlay-name',
validate: value => value && typeof value === 'string',
});
}
/**
* Reads button type from DOM, stores it in this.type. Defaults to 'toggle'.
* @private
*/
getType() {
return getAndValidateAttribute({
element: this,
name: 'data-type',
validate: value => !value || ['toggle', 'open', 'close'].includes(value),
}) || 'toggle';
}
/**
* @private
*/
setupClickListener() {
createListener(this, 'click', this.handleClick.bind(this));
}
/**
* @private
*/
handleClick() {
this.model[this.type]();
}
/**
* @private
*/
handleModelChanges() {
this.model.on('change', this.updateDOM.bind(this));
}
/**
* @private
*/
updateDOM() {
/* global requestAnimationFrame */
requestAnimationFrame(() => {
if (this.model.isOpen) {
this.classList.remove(this.closedClass);
this.classList.add(this.openClass);
} else {
this.classList.remove(this.openClass);
this.classList.add(this.closedClass);
}
});
}
}
/* global window */
if (!window.customElements.get('overlay-button-component')) {
window.customElements.define('overlay-button-component', OverlayButton);
}
}());

@@ -1,6 +0,309 @@

import Overlay from './Overlay.js';
(function () {
'use strict';
/* global window */
if (!window.customElements.get('overlay-component')) {
window.customElements.define('overlay-component', Overlay);
}
/**
* Simplifies watching attributes; pass in a config and this mixin will automatically store
* attribute values in a component to reduce DOM reads and simplify validation.
* IMPORTANT: We might want to use observable attributes in the future; we did not do so now,
* because
* a) it's hard to add the static method to he class that consumes the mixin
* b) there is no JSDOM support for observable attributes, which makes testing a pain
* @param {object[]} config Attribute config; each entry may consist of the following
* properties:
* - name (string, mandatory): Name of the attribute to watch
* - validate (function, optional): Validation function; return a
* falsy value if validation is not passed
* - property (string, optional): Class property that the value
* should be stored in; if not set, name will be used instead
* - transform (function): Transforms value before it is saved as a
* property
*/
var canReadAttributes = (config) => {
if (!config.every(item => item.name)) {
throw new Error(`canReadAttribute: Every config entry must be an object with property name; you passed ${JSON.stringify(config)} instead.`);
}
return {
readAttributes() {
config.forEach((attributeConfig) => {
const {
name,
validate,
property,
transform,
} = attributeConfig;
// Use getAttribute instead of dataset, as attribute is not guaranteed to start
// with data-
const value = this.getAttribute(name);
if (typeof validate === 'function' && !validate(value)) {
throw new Error(`canWatchAttribute: Attribute ${name} does not match validation rules`);
}
const transformFunction = transform || (initialValue => initialValue);
const propertyName = property || name;
this[propertyName] = transformFunction(value);
});
},
};
};
/**
* Adds event listener to an element and returns removeEventListener function that only needs to
* be called to de-register an event.
* @example
* const disposer = createListener(window, 'click', () => {});
*/
var createListener = (element, eventName, handler) => {
// Takes this from execution context which must be the custom element
element.addEventListener(eventName, handler);
return () => element.removeEventListener(eventName, handler);
};
/**
* Mixin for a component that announces itself by dispatching an event
* @example
* class extends HTMLElement {
* constructor() {
* Object.assign(this, canRegisterElements({ eventTarget: this }));
* }
* connectedCallback() {
* this.registerAnnouncements();
* }
* }
* */
var canRegisterElements = ({
eventName = 'announce-element',
eventTarget = window, // Does that work?
eventType,
eventIdentifier,
model,
} = {}) => (
{
registerAnnouncements() {
eventTarget.addEventListener(eventName, (ev) => {
const { detail } = ev;
if (eventType && detail.eventType !== eventType) return;
if (eventIdentifier && detail.eventIdentifier !== eventIdentifier) return;
const { element } = ev.detail;
if (typeof element.setModel !== 'function') {
console.warn(`canRegisterElement: setModel is not a function on announcing element, but ${element.setModel}.`);
} else {
element.setModel(model);
}
});
},
}
);
/**
* Simple EventEmitter mixin; use our own implementation as a) most NPM modules don't provide an
* ES6 export and b) they're not made to be used as mixins.
* Export a function for all mixins, even if not needed here (consistency).
*/
var canEmitEvents = () => {
return {
/**
* Map that holds all callbacks for all types
* @type Map.<*, function[]>
*/
eventHandlers: new Map(),
/**
* Adds event handler for a given type
* @param {*} type Name of the event handler
* @param {function} callback Callback to call if corresponding event is emitted
*/
on(type, callback) {
if (!this.eventHandlers.has(type)) this.eventHandlers.set(type, [callback]);
else this.eventHandlers.get(type).push(callback);
},
/**
* Removes an event handler; if only type is given, all callbacks of the type will be
* removed. If type and callback are given, only the specific callbacks for the given type
* will be removed.
* @param {*} type Type of event handler to remove
* @param {function} callback Callback to remove
*/
off(type, callback) {
if (!this.eventHandlers.has(type)) return;
if (!callback) this.eventHandlers.delete(type);
else {
this.eventHandlers.set(
type,
this.eventHandlers.get(type).filter(cb => cb !== callback),
);
}
},
/**
* Calls all callbacks of the provided type with the given parameters.
* @param {*} type Type of eventHandler to call
* @param {...*} params Parameters to pass to callbacks
*/
emit(type, ...params) {
(this.eventHandlers.get(type) || []).forEach(handler => handler(...params));
},
};
};
class OverlayModel {
isOverlayOpen = false;
constructor() {
Object.assign(this, canEmitEvents());
}
open() {
// Prevent unnecessarily emitted event
if (this.isOverlayOpen) return;
this.isOverlayOpen = true;
this.emit('change');
}
close() {
// Prevent unnecessarily emitted event
if (!this.isOverlayOpen) return;
this.isOverlayOpen = false;
this.emit('change');
}
toggle() {
this.isOverlayOpen = !this.isOverlayOpen;
this.emit('change');
}
get isOpen() {
return this.isOverlayOpen;
}
}
/* global HTMLElement, window, document, CustomEvent */
/**
* Overlay that is opened/closed by open/closeoverlay events. Optionally closes on esc or
* click outside and always locks background (prevents scrolling).
*/
class Overlay extends HTMLElement {
constructor() {
super();
this.model = new OverlayModel();
Object.assign(
this,
canReadAttributes([{
name: 'data-name',
validate: value => !!value,
property: 'name',
}, {
name: 'data-background-selector',
property: 'backgroundSelector',
}, {
name: 'data-background-visible-class-name',
property: 'backgroundVisibleClassName',
}, {
name: 'data-visible-class-name',
validate: value => !!value,
property: 'visibleClassName',
}, {
name: 'data-disable-esc',
property: 'disableEsc',
// Create bool
transform: value => !!value,
}, {
name: 'data-disable-click-outside',
property: 'disableClickOutside',
transform: value => !!value,
}]),
canRegisterElements({
eventType: 'overlay-button',
eventIdentifier: this.getAttribute('data-name'),
eventTarget: window,
model: this.model,
}),
);
this.readAttributes();
this.registerAnnouncements();
this.setupModelListeners();
this.updateDOM();
}
connectedCallback() {
if (this.backgroundSelector) {
this.background = document.querySelector(this.backgroundSelector);
}
}
disconnectedCallback() {
this.background = null;
}
handleKeyDown(event) {
if (event.keyCode === 27 && !this.disableEsc) this.model.close();
}
handleClickOutside(event) {
if (this.disableClickOutside) return;
const { target } = event;
// Test if target is a child of overlay
if (this.contains(target)) return;
this.model.close();
}
/**
* Listens to model
* @private
*/
setupModelListeners() {
this.model.on('change', this.updateDOM.bind(this));
}
updateDOM() {
window.requestAnimationFrame(() => {
const visible = this.model.isOpen;
if (visible) {
this.classList.add(this.visibleClassName);
if (this.background && this.backgroundVisibleClassName) {
this.background.classList.add(this.backgroundVisibleClassName);
}
this.dispatchEvent(new CustomEvent('open'));
} else {
this.classList.remove(this.visibleClassName);
if (this.background && this.backgroundVisibleClassName) {
this.background.classList.remove(this.backgroundVisibleClassName);
}
this.dispatchEvent(new CustomEvent('close'));
}
});
setTimeout(() => {
if (this.model.isOpen) {
// Only add esc/click on open or click on open button will at the same time close
// the overlay
this.disconnectEsc = createListener(window, 'keydown', this.handleKeyDown.bind(this));
this.disconnectClick = createListener(window, 'click', this.handleClickOutside.bind(this));
} else {
if (this.disconnectEsc) this.disconnectEsc();
if (this.disconnectClick) this.disconnectClick();
}
});
}
}
/* global window */
if (!window.customElements.get('overlay-component')) {
window.customElements.define('overlay-component', Overlay);
}
}());

9

package.json
{
"name": "@joinbox/overlay",
"version": "1.0.1",
"version": "1.0.2",
"description": "Overlay component that can be opened and closed via OverlayButtons",
"main": "Overlay.js",
"scripts": {
"test": "npx ava --verbose"
"test": "npx ava --verbose",
"build": "npx rollup --c rollup.config.js"
},

@@ -26,3 +27,3 @@ "author": "Felix Steiner <felix@joinbox.com>",

"lerna": "^4.0.0",
"rollup": "^2.26.6"
"rollup": "^2.50.5"
},

@@ -32,3 +33,3 @@ "publishConfig": {

},
"gitHead": "1c9840334b6fdac6d2f947acaa84457df104a655"
"gitHead": "f39707c916e54530c32827a26639ef5a3090ecd2"
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc