@shopify/app-bridge-core
Version:
**[Join our team and work on libraries like this one.](https://www.shopify.ca/careers)**
239 lines (236 loc) • 9.08 kB
JavaScript
import { LifecycleHook } from '../client/types.js';
import { removeFromCollection } from '../util/collection.js';
import { throwError, Action, isErrorEventName } from './Error/index.js';
import { Group } from './types.js';
import { generateUuid } from './uuid.js';
import { forEachInEnum, getEventNameSpace } from './helper.js';
class ActionSet {
app;
type;
group;
id;
defaultGroup;
subgroups = [];
subscriptions = [];
constructor(app, type, group, id) {
this.app = app;
this.type = type;
this.group = group;
if (!app) {
throwError(Action.INVALID_ACTION, 'Missing required `app`');
}
this.id = id || generateUuid();
this.defaultGroup = group;
const defaultSet = this.set;
this.set = (...args) => {
if (!this.app.hooks) {
return defaultSet.apply(this, args);
}
return this.app.hooks.run(LifecycleHook.UpdateAction, defaultSet, this, ...args);
};
}
set(..._) { }
get component() {
return {
id: this.id,
subgroups: this.subgroups,
type: this.type,
};
}
updateSubscription(subscriptionToRemove, group, subgroups) {
const { eventType, callback, component } = subscriptionToRemove;
let currentIndex;
currentIndex = this.subscriptions.findIndex((subscription) => subscription === subscriptionToRemove);
if (currentIndex >= 0) {
this.subscriptions[currentIndex].unsubscribe();
}
else {
currentIndex = undefined;
}
this.group = group;
this.subgroups = subgroups;
Object.assign(component, { subgroups: this.subgroups });
return this.subscribe(eventType, callback, component, currentIndex);
}
error(callback) {
const subscriptionIndices = [];
forEachInEnum(Action, (eventNameSpace) => {
// Keep track of subscription index so we can call unsubscribe later
// This ensure it will continue to work even when the subscription has been updated
subscriptionIndices.push(this.subscriptions.length);
this.subscribe(eventNameSpace, callback);
});
return () => {
const subscriptionsToRemove = subscriptionIndices.map((index) => this.subscriptions[index]);
subscriptionsToRemove.forEach((toRemove) => {
removeFromCollection(this.subscriptions, toRemove, (removed) => {
removed.unsubscribe();
});
});
};
}
subscribe(eventName, callback, component, currentIndex) {
const eventComponent = component || this.component;
const eventType = eventName.toUpperCase();
const boundedCallback = typeof currentIndex === 'number' ? callback : callback.bind(this);
let eventNameSpace;
if (isErrorEventName(eventName)) {
eventNameSpace = getEventNameSpace(Group.Error, eventName, {
...eventComponent,
type: '',
});
}
else {
eventNameSpace = getEventNameSpace(this.group, eventName, eventComponent);
}
const unsubscribe = this.app.subscribe(eventNameSpace, boundedCallback, component ? component.id : this.id);
const subscription = {
eventType,
unsubscribe,
callback: boundedCallback,
component: eventComponent,
updateSubscribe: (group, subgroups) => this.updateSubscription(subscription, group, subgroups),
};
if (typeof currentIndex === 'number' &&
currentIndex >= 0 &&
currentIndex < this.subscriptions.length) {
this.subscriptions[currentIndex] = subscription;
}
else {
this.subscriptions.push(subscription);
}
return unsubscribe;
}
unsubscribe(resetOnly = false) {
unsubscribeActions(this.subscriptions, this.defaultGroup, resetOnly);
return this;
}
}
class ActionSetWithChildren extends ActionSet {
children = [];
unsubscribe(unsubscribeChildren = true, resetParentOnly = false) {
unsubscribeActions(this.subscriptions, this.defaultGroup, resetParentOnly);
this.children.forEach((child) => {
if (child instanceof ActionSetWithChildren) {
child.unsubscribe(unsubscribeChildren, !unsubscribeChildren);
}
else {
child.unsubscribe(!unsubscribeChildren);
}
});
return this;
}
getChild(id) {
const childIndex = this.children.findIndex((child) => child.id === id);
return childIndex >= 0 ? this.children[childIndex] : undefined;
}
getChildIndex(id) {
return this.children.findIndex((child) => child.id === id);
}
getChildSubscriptions(id, eventType) {
return this.subscriptions.filter((sub) => sub.component.id === id && (!eventType || eventType === sub.eventType));
}
addChild(child, group, subgroups) {
const { subscriptions } = child;
const existingChild = this.getChild(child.id);
// Add child if it doesn't already exist
if (!existingChild) {
this.children.push(child);
}
if (!subscriptions || (group === child.group && subgroups === child.subgroups)) {
return this;
}
subscriptions.forEach((subscription) => {
const { updateSubscribe } = subscription;
updateSubscribe(group, subgroups);
});
// Update child's group and subgroups
Object.assign(child, { group, subgroups });
// Update child's children subscriptions
if (child instanceof ActionSetWithChildren) {
child.children.forEach((childIter) => this.addChild(childIter, group, subgroups));
}
return this;
}
removeChild(id) {
removeFromCollection(this.children, this.getChild(id), () => {
const toBeRemoved = this.subscriptions.filter((subs) => subs.component.id === id);
toBeRemoved.forEach((toRemove) => {
removeFromCollection(this.subscriptions, toRemove, (removed) => {
removed.unsubscribe();
});
});
});
return this;
}
subscribeToChild(child, eventName, callback) {
const boundedCallback = callback.bind(this);
if (eventName instanceof Array) {
eventName.forEach((eventNameIter) => this.subscribeToChild(child, eventNameIter, callback));
return this;
}
if (typeof eventName !== 'string') {
return this;
}
const eventType = eventName.toUpperCase();
const currentSubscriptions = this.getChildSubscriptions(child.id, eventType);
if (currentSubscriptions.length > 0) {
// Subscription is already there, simply update it
currentSubscriptions.forEach((subs) => subs.updateSubscribe(this.group, child.subgroups));
}
else {
const childComponent = {
id: child.id,
subgroups: child.subgroups,
type: child.type,
};
this.subscribe(eventType, boundedCallback, childComponent);
}
return this;
}
getUpdatedChildActions(newActions, currentActions) {
if (newActions.length === 0) {
while (currentActions.length > 0) {
const action = currentActions.pop();
if (!action) {
break;
}
this.removeChild(action.id);
}
return undefined;
}
// Only allow unique actions
const uniqueActions = newActions.filter((action, index, actionsArr) => index === actionsArr.indexOf(action));
const newActionIds = uniqueActions.map((action) => action.id);
// Remove unused actions
const unusedActions = currentActions.filter((action) => {
return newActionIds.indexOf(action.id) < 0;
});
while (unusedActions.length > 0) {
const action = unusedActions.pop();
if (!action) {
break;
}
this.removeChild(action.id);
}
return uniqueActions;
}
}
function unsubscribeActions(subscriptions, defaultGroup, reassign = false) {
subscriptions.forEach((subscription) => {
if (reassign) {
const { updateSubscribe } = subscription;
// eslint-disable-next-line no-warning-comments
// TODO: Support cases where we don't wipe out group and subgroups to defaults
updateSubscribe(defaultGroup, []);
}
else {
const { unsubscribe } = subscription;
unsubscribe();
}
});
if (!reassign) {
subscriptions.length = 0;
}
}
export { ActionSet, ActionSetWithChildren, unsubscribeActions };