Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@logseq/libs

Package Overview
Dependencies
Maintainers
6
Versions
69
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@logseq/libs - npm Package Compare versions

Comparing version
0.0.15
to
0.0.16
+13
-1
CHANGELOG.md

@@ -7,4 +7,16 @@ # Changelog

## [0.0.16]
### Added
- Support api of `logseq.UI.queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>`
- Support api of `logseq.UI.queryElementById: (id: string) => Promise<string | boolean>`
- Support api of `logseq.UI.checkSlotValid: (slot: UISlotIdentity['slot']) => Promise<boolean>`
- Support api of `logseq.UI.resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<any>`
- Support api of `logseq.Assets.builtInOpen(path: string): Promise<boolean | undefined>`
### Fixed
- fix Plugin can't register command shortcut with editing mode [#10392](https://github.com/logseq/logseq/issues/10392)
- fix [Plugin API] [Keymap] Command without keybinding can't be present in Keymap [#10466](https://github.com/logseq/logseq/issues/10466)
- fix [Possible DATA LOSS] [Plugin API] [Keymap] Any plugin could break the global config.edn [#10465](https://github.com/logseq/logseq/issues/10465)
## [0.0.15]
### Added

@@ -11,0 +23,0 @@ - Support a plug-in flag for the plugin slash commands item

+3
-8

@@ -22,9 +22,3 @@ /// <reference types="node" />

export declare function isObject(item: any): boolean;
export declare const deepMerge: {
<TObject, TSource>(object: TObject, source: TSource): TObject & TSource;
<TObject_1, TSource1, TSource2>(object: TObject_1, source1: TSource1, source2: TSource2): TObject_1 & TSource1 & TSource2;
<TObject_2, TSource1_1, TSource2_1, TSource3>(object: TObject_2, source1: TSource1_1, source2: TSource2_1, source3: TSource3): TObject_2 & TSource1_1 & TSource2_1 & TSource3;
<TObject_3, TSource1_2, TSource2_2, TSource3_1, TSource4>(object: TObject_3, source1: TSource1_2, source2: TSource2_2, source3: TSource3_1, source4: TSource4): TObject_3 & TSource1_2 & TSource2_2 & TSource3_1 & TSource4;
(object: any, ...otherArgs: any[]): any;
};
export declare function deepMerge<T>(a: Partial<T>, b: Partial<T>): T;
export declare class PluginLogger extends EventEmitter<'change'> {

@@ -69,3 +63,3 @@ private _tag?;

float: boolean;
}) => void): () => void;
}) => void): false | (() => void);
export declare function cleanInjectedUI(id: string): void;

@@ -76,1 +70,2 @@ export declare function cleanInjectedScripts(this: PluginLocal): void;

export declare function mergeSettingsWithSchema(settings: Record<string, any>, schema: Array<SettingSchemaDesc>): Record<string, any>;
export declare function normalizeKeyStr(s: string): string;
import EventEmitter from 'eventemitter3';
import { deferred, PluginLogger } from './helpers';
import * as pluginHelpers from './helpers';
import DOMPurify from 'dompurify';
import { LSPluginCaller } from './LSPlugin.caller';

@@ -9,2 +10,3 @@ import { ILSPluginThemeManager, LegacyTheme, SettingSchemaDesc, Theme, ThemeMode } from './LSPlugin';

LSPluginCore: LSPluginCore;
DOMPurify: typeof DOMPurify;
}

@@ -11,0 +13,0 @@ }

@@ -28,3 +28,2 @@ import * as CSS from 'csstype';

resizable: boolean;
[key: string]: any;
};

@@ -63,5 +62,15 @@ export declare type UIBaseOptions = {

icon: string;
[key: string]: any;
/**
* Alternative entrypoint for development.
*/
devEntry: unknown;
/**
* For legacy themes, do not use.
*/
theme: unknown;
}
export interface LSPluginBaseInfo {
/**
* Must be unique.
*/
id: string;

@@ -71,5 +80,12 @@ mode: 'shadow' | 'iframe';

disabled: boolean;
[key: string]: any;
};
[key: string]: any;
} & Record<string, unknown>;
effect: boolean;
/**
* For internal use only. Indicates if plugin is installed in dot root.
*/
iir: boolean;
/**
* For internal use only.
*/
lsr: string;
}

@@ -122,3 +138,2 @@ export declare type IHookEvent = {

enabledJournals: boolean;
[key: string]: any;
}

@@ -132,3 +147,2 @@ /**

path: string;
[key: string]: any;
}

@@ -144,3 +158,2 @@ /**

parent: IEntityID;
unordered: boolean;
content: string;

@@ -162,3 +175,3 @@ page: IEntityID;

title?: Array<any>;
[key: string]: any;
marker?: string;
}

@@ -181,3 +194,2 @@ /**

updatedAt?: number;
[key: string]: any;
}

@@ -200,5 +212,6 @@ export declare type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>;

};
export declare type Keybinding = string | Array<string>;
export declare type SimpleCommandKeybinding = {
mode?: 'global' | 'non-editing' | 'editing';
binding: string;
binding: Keybinding;
mac?: string;

@@ -216,3 +229,3 @@ };

};
export declare type ExternalCommandType = 'logseq.command/run' | 'logseq.editor/cycle-todo' | 'logseq.editor/down' | 'logseq.editor/up' | 'logseq.editor/expand-block-children' | 'logseq.editor/collapse-block-children' | 'logseq.editor/open-file-in-default-app' | 'logseq.editor/open-file-in-directory' | 'logseq.editor/select-all-blocks' | 'logseq.editor/toggle-open-blocks' | 'logseq.editor/zoom-in' | 'logseq.editor/zoom-out' | 'logseq.editor/indent' | 'logseq.editor/outdent' | 'logseq.editor/copy' | 'logseq.editor/cut' | 'logseq.go/home' | 'logseq.go/journals' | 'logseq.go/keyboard-shortcuts' | 'logseq.go/next-journal' | 'logseq.go/prev-journal' | 'logseq.go/search' | 'logseq.go/search-in-page' | 'logseq.go/tomorrow' | 'logseq.go/backward' | 'logseq.go/forward' | 'logseq.search/re-index' | 'logseq.sidebar/clear' | 'logseq.sidebar/open-today-page' | 'logseq.ui/goto-plugins' | 'logseq.ui/select-theme-color' | 'logseq.ui/toggle-brackets' | 'logseq.ui/toggle-cards' | 'logseq.ui/toggle-contents' | 'logseq.ui/toggle-document-mode' | 'logseq.ui/toggle-help' | 'logseq.ui/toggle-left-sidebar' | 'logseq.ui/toggle-right-sidebar' | 'logseq.ui/toggle-settings' | 'logseq.ui/toggle-theme' | 'logseq.ui/toggle-wide-mode' | 'logseq.command-palette/toggle';
export declare type ExternalCommandType = 'logseq.command/run' | 'logseq.editor/cycle-todo' | 'logseq.editor/down' | 'logseq.editor/up' | 'logseq.editor/expand-block-children' | 'logseq.editor/collapse-block-children' | 'logseq.editor/open-file-in-default-app' | 'logseq.editor/open-file-in-directory' | 'logseq.editor/select-all-blocks' | 'logseq.editor/toggle-open-blocks' | 'logseq.editor/zoom-in' | 'logseq.editor/zoom-out' | 'logseq.editor/indent' | 'logseq.editor/outdent' | 'logseq.editor/copy' | 'logseq.editor/cut' | 'logseq.go/home' | 'logseq.go/journals' | 'logseq.go/keyboard-shortcuts' | 'logseq.go/next-journal' | 'logseq.go/prev-journal' | 'logseq.go/search' | 'logseq.go/tomorrow' | 'logseq.go/backward' | 'logseq.go/forward' | 'logseq.search/re-index' | 'logseq.sidebar/clear' | 'logseq.sidebar/open-today-page' | 'logseq.ui/goto-plugins' | 'logseq.ui/select-theme-color' | 'logseq.ui/toggle-brackets' | 'logseq.ui/toggle-contents' | 'logseq.ui/toggle-document-mode' | 'logseq.ui/toggle-help' | 'logseq.ui/toggle-left-sidebar' | 'logseq.ui/toggle-right-sidebar' | 'logseq.ui/toggle-settings' | 'logseq.ui/toggle-theme' | 'logseq.ui/toggle-wide-mode';
export declare type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets';

@@ -278,3 +291,8 @@ export declare type SearchIndiceInitStatus = boolean;

*/
registerCommandShortcut: (keybinding: SimpleCommandKeybinding, action: SimpleCommandCallback) => void;
registerCommandShortcut: (keybinding: SimpleCommandKeybinding | string, action: SimpleCommandCallback, opts?: Partial<{
key: string;
label: string;
desc: string;
extras: Record<string, any>;
}>) => void;
/**

@@ -335,14 +353,2 @@ * Supported all registered palette commands

insertTemplate: (target: BlockUUID, name: string) => Promise<any>;
queryElementById: (id: string) => Promise<string | boolean>;
/**
* @added 0.0.5
* @param selector
*/
queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>;
/**
* @deprecated Use `logseq.UI.showMsg` instead
* @param content
* @param status
*/
showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string) => void;
setZoomFactor: (factor: number) => void;

@@ -378,2 +384,4 @@ setFullScreen: (flag: boolean | 'toggle') => void;

}>;
onBeforeCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook;
onAfterCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook;
/**

@@ -592,3 +600,3 @@ * provide ui slot to specific block with UUID

}) => void;
openInRightSidebar: (uuid: BlockUUID) => void;
openInRightSidebar: (id: BlockUUID | EntityID) => void;
/**

@@ -667,11 +675,8 @@ * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator

export interface IUIProxy {
/**
* @added 0.0.2
*
* @param content
* @param status
* @param opts
*/
showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string, opts?: Partial<UIMsgOptions>) => Promise<UIMsgKey>;
closeMsg: (key: UIMsgKey) => void;
queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>;
queryElementById: (id: string) => Promise<string | boolean>;
checkSlotValid: (slot: UISlotIdentity['slot']) => Promise<boolean>;
resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<Record<string, string | undefined> | null>;
}

@@ -705,2 +710,8 @@ /**

makeUrl(path: string): Promise<string>;
/**
* try to open asset type file in Logseq app
* @added 0.0.16
* @param path
*/
builtInOpen(path: string): Promise<boolean | undefined>;
}

@@ -841,4 +852,4 @@ export interface ILSPluginThemeManager {

resolveResourceFullUrl(filePath: string): string;
App: IAppProxy & Record<string, any>;
Editor: IEditorProxy & Record<string, any>;
App: IAppProxy;
Editor: IEditorProxy;
DB: IDBProxy;

@@ -845,0 +856,0 @@ Git: IGitProxy;

@@ -78,5 +78,4 @@ import { PluginLogger } from './helpers';

get settings(): {
[key: string]: any;
disabled: boolean;
};
} & Record<string, unknown>;
get caller(): LSPluginCaller;

@@ -83,0 +82,0 @@ resolveResourceFullUrl(filePath: string): string;

{
"name": "@logseq/libs",
"version": "0.0.15",
"version": "0.0.16",
"description": "Logseq SDK libraries",

@@ -21,2 +21,3 @@ "main": "dist/lsplugin.user.js",

"debug": "4.3.4",
"deepmerge": "4.3.1",
"dompurify": "2.3.8",

@@ -23,0 +24,0 @@ "eventemitter3": "4.0.7",

import { safeSnakeCase } from '../helpers';
/**
* WARN: These are some experience features and may be adjusted at any time.
* These unofficial plugins that use these APIs are temporarily
* not supported on the Marketplace.
*/
export class LSPluginExperiments {
ctx;
constructor(ctx) {
this.ctx = ctx;
}
get React() {
return this.ensureHostScope().React;
}
get ReactDOM() {
return this.ensureHostScope().ReactDOM;
}
get pluginLocal() {
return this.ensureHostScope().LSPluginCore.ensurePlugin(this.ctx.baseInfo.id);
}
invokeExperMethod(type, ...args) {
const host = this.ensureHostScope();
type = safeSnakeCase(type)?.toLowerCase();
return host.logseq.api['exper_' + type]?.apply(host, args);
}
async loadScripts(...scripts) {
scripts = scripts.map((it) => {
if (!it?.startsWith('http')) {
return this.ctx.resolveResourceFullUrl(it);
}
return it;
});
scripts.unshift(this.ctx.baseInfo.id);
await this.invokeExperMethod('loadScripts', ...scripts);
}
registerFencedCodeRenderer(type, opts) {
return this.ensureHostScope().logseq.api.exper_register_fenced_code_renderer(this.ctx.baseInfo.id, type, opts);
}
registerExtensionsEnhancer(type, enhancer) {
const host = this.ensureHostScope();
switch (type) {
case 'katex':
if (host.katex) {
enhancer(host.katex).catch(console.error);
}
break;
default:
}
return host.logseq.api.exper_register_extensions_enhancer(this.ctx.baseInfo.id, type, enhancer);
}
ensureHostScope() {
if (window === top) {
throw new Error('Can not access host scope!');
}
return top;
}
}
import { EventEmitter } from 'eventemitter3';
const CLIENT_MSG_CALLBACK = '#lsp#request#callback';
const genTaskCallbackType = (id) => `task_callback_${id}`;
/**
* Request task
*/
export class LSPluginRequestTask {
_client;
_requestId;
_requestOptions;
_promise;
_aborted = false;
constructor(_client, _requestId, _requestOptions = {}) {
this._client = _client;
this._requestId = _requestId;
this._requestOptions = _requestOptions;
this._promise = new Promise((resolve, reject) => {
if (!this._requestId) {
return reject(null);
}
// task result listener
this._client.once(genTaskCallbackType(this._requestId), (e) => {
if (e && e instanceof Error) {
reject(e);
}
else {
resolve(e);
}
});
});
const { success, fail, final } = this._requestOptions;
this._promise
.then((res) => {
success?.(res);
})
.catch((e) => {
fail?.(e);
})
.finally(() => {
final?.();
});
}
abort() {
if (!this._requestOptions.abortable ||
this._aborted)
return;
this._client.ctx._execCallableAPI('http_request_abort', this._requestId);
this._aborted = true;
}
get promise() {
return this._promise;
}
get client() {
return this._client;
}
get requestId() {
return this._requestId;
}
}
/**
* A simple request client
*/
export class LSPluginRequest extends EventEmitter {
_ctx;
constructor(_ctx) {
super();
this._ctx = _ctx;
// request callback listener
this.ctx.caller.on(CLIENT_MSG_CALLBACK, (e) => {
const reqId = e?.requestId;
if (!reqId)
return;
this.emit(genTaskCallbackType(reqId), e?.payload);
});
}
static createRequestTask(client, requestID, requestOptions) {
return new LSPluginRequestTask(client, requestID, requestOptions);
}
async _request(options) {
const pid = this.ctx.baseInfo.id;
const { success, fail, final, ...requestOptions } = options;
const reqID = this.ctx.Experiments.invokeExperMethod('request', pid, requestOptions);
const task = LSPluginRequest.createRequestTask(this.ctx.Request, reqID, options);
if (!requestOptions.abortable) {
return task.promise;
}
return task;
}
get ctx() {
return this._ctx;
}
}
import { isArray, isFunction, mapKeys } from 'lodash-es';
export class LSPluginSearchService {
ctx;
serviceHooks;
/**
* @param ctx
* @param serviceHooks
*/
constructor(ctx, serviceHooks) {
this.ctx = ctx;
this.serviceHooks = serviceHooks;
ctx._execCallableAPI('register-search-service', ctx.baseInfo.id, serviceHooks.name, serviceHooks.options);
// hook events TODO: remove listeners
const wrapHookEvent = (k) => `service:search:${k}:${serviceHooks.name}`;
Object.entries({
query: {
f: 'onQuery', args: ['graph', 'q', true], reply: true,
transformOutput: (data) => {
// TODO: transform keys?
if (isArray(data?.blocks)) {
data.blocks = data.blocks.map(it => {
return it && mapKeys(it, (_, k) => `block/${k}`);
});
}
return data;
}
},
rebuildBlocksIndice: { f: 'onIndiceInit', args: ['graph', 'blocks'] },
transactBlocks: { f: 'onBlocksChanged', args: ['graph', 'data'] },
truncateBlocks: { f: 'onIndiceReset', args: ['graph'] },
removeDb: { f: 'onGraph', args: ['graph'] }
}).forEach(([k, v]) => {
const hookEvent = wrapHookEvent(k);
ctx.caller.on(hookEvent, async (payload) => {
if (isFunction(serviceHooks?.[v.f])) {
let ret = null;
try {
ret = await serviceHooks[v.f].apply(serviceHooks, (v.args || []).map((prop) => {
if (!payload)
return;
if (prop === true)
return payload;
if (payload.hasOwnProperty(prop)) {
const ret = payload[prop];
delete payload[prop];
return ret;
}
}));
if (v.transformOutput) {
ret = v.transformOutput(ret);
}
}
catch (e) {
console.error('[SearchService] ', e);
ret = e;
}
finally {
if (v.reply) {
ctx.caller.call(`${hookEvent}:reply`, ret);
}
}
}
});
});
}
}
/**
* A storage based on local files under specific context
*/
class LSPluginFileStorage {
ctx;
opts;
/**
* @param ctx
* @param opts
*/
constructor(ctx, opts) {
this.ctx = ctx;
this.opts = opts;
}
/**
* plugin id
*/
get ctxId() {
return this.ctx.baseInfo.id;
}
/**
* @param key A string as file name that support nested directory
* @param value Storage value
*/
setItem(key, value) {
return this.ctx.caller.callAsync(`api:call`, {
method: 'write-plugin-storage-file',
args: [this.ctxId, key, value, this.opts?.assets],
});
}
/**
* @param key
*/
getItem(key) {
return this.ctx.caller.callAsync(`api:call`, {
method: 'read-plugin-storage-file',
args: [this.ctxId, key, this.opts?.assets],
});
}
/**
* @param key
*/
removeItem(key) {
return this.ctx.caller.call(`api:call`, {
method: 'unlink-plugin-storage-file',
args: [this.ctxId, key, this.opts?.assets],
});
}
/**
* Get all path file keys
*/
allKeys() {
return this.ctx.caller.callAsync(`api:call`, {
method: 'list-plugin-storage-files',
args: [this.ctxId, this.opts?.assets]
});
}
/**
* Clears the storage
*/
clear() {
return this.ctx.caller.call(`api:call`, {
method: 'clear-plugin-storage-files',
args: [this.ctxId, this.opts?.assets],
});
}
/**
* @param key
*/
hasItem(key) {
return this.ctx.caller.callAsync(`api:call`, {
method: 'exist-plugin-storage-file',
args: [this.ctxId, key, this.opts?.assets],
});
}
}
export { LSPluginFileStorage };
// Fork from https://github.com/dollarshaveclub/postmate
/**
* The type of messages our frames our sending
* @type {String}
*/
export const messageType = 'application/x-postmate-v1+json';
/**
* The maximum number of attempts to send a handshake request to the parent
* @type {Number}
*/
export const maxHandshakeRequests = 5;
/**
* A unique message ID that is used to ensure responses are sent to the correct requests
* @type {Number}
*/
let _messageId = 0;
/**
* Increments and returns a message ID
* @return {Number} A unique ID for a message
*/
export const generateNewMessageId = () => ++_messageId;
/**
* Postmate logging function that enables/disables via config
*/
export const log = (...args) => (Postmate.debug ? console.log(...args) : null);
/**
* Takes a URL and returns the origin
* @param {String} url The full URL being requested
* @return {String} The URLs origin
*/
export const resolveOrigin = (url) => {
const a = document.createElement('a');
a.href = url;
const protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol;
const host = a.host.length
? a.port === '80' || a.port === '443'
? a.hostname
: a.host
: window.location.host;
return a.origin || `${protocol}//${host}`;
};
const messageTypes = {
handshake: 1,
'handshake-reply': 1,
call: 1,
emit: 1,
reply: 1,
request: 1,
};
/**
* Ensures that a message is safe to interpret
* @param {Object} message The postmate message being sent
* @param {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check
* @return {Boolean}
*/
export const sanitize = (message, allowedOrigin) => {
if (typeof allowedOrigin === 'string' && message.origin !== allowedOrigin)
return false;
if (!message.data)
return false;
if (typeof message.data === 'object' && !('postmate' in message.data))
return false;
if (message.data.type !== messageType)
return false;
if (!messageTypes[message.data.postmate])
return false;
return true;
};
/**
* Takes a model, and searches for a value by the property
* @param {Object} model The dictionary to search against
* @param {String} property A path within a dictionary (i.e. 'window.location.href')
* passed to functions in the child model
* @return {Promise}
*/
export const resolveValue = (model, property, args) => {
const unwrappedContext = typeof model[property] === 'function' ? model[property].apply(null, args) : model[property];
return Promise.resolve(unwrappedContext);
};
/**
* Composes an API to be used by the parent
* @param {Object} info Information on the consumer
*/
export class ParentAPI {
parent;
frame;
child;
events = {};
childOrigin;
listener;
constructor(info) {
this.parent = info.parent;
this.frame = info.frame;
this.child = info.child;
this.childOrigin = info.childOrigin;
if (process.env.NODE_ENV !== 'production') {
log('Parent: Registering API');
log('Parent: Awaiting messages...');
}
this.listener = (e) => {
if (!sanitize(e, this.childOrigin))
return false;
/**
* the assignments below ensures that e, data, and value are all defined
*/
const { data, name } = ((e || {}).data || {}).value || {};
if (e.data.postmate === 'emit') {
if (process.env.NODE_ENV !== 'production') {
log(`Parent: Received event emission: ${name}`);
}
if (name in this.events) {
this.events[name].forEach((callback) => {
callback.call(this, data);
});
}
}
};
this.parent.addEventListener('message', this.listener, false);
if (process.env.NODE_ENV !== 'production') {
log('Parent: Awaiting event emissions from Child');
}
}
get(property, ...args) {
return new Promise((resolve) => {
// Extract data from response and kill listeners
const uid = generateNewMessageId();
const transact = (e) => {
if (e.data.uid === uid && e.data.postmate === 'reply') {
this.parent.removeEventListener('message', transact, false);
resolve(e.data.value);
}
};
// Prepare for response from Child...
this.parent.addEventListener('message', transact, false);
// Then ask child for information
this.child.postMessage({
postmate: 'request',
type: messageType,
property,
args,
uid,
}, this.childOrigin);
});
}
call(property, data) {
// Send information to the child
this.child.postMessage({
postmate: 'call',
type: messageType,
property,
data,
}, this.childOrigin);
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
destroy() {
if (process.env.NODE_ENV !== 'production') {
log('Parent: Destroying Postmate instance');
}
window.removeEventListener('message', this.listener, false);
this.frame.parentNode.removeChild(this.frame);
}
}
/**
* Composes an API to be used by the child
* @param {Object} info Information on the consumer
*/
export class ChildAPI {
model;
parent;
parentOrigin;
child;
constructor(info) {
this.model = info.model;
this.parent = info.parent;
this.parentOrigin = info.parentOrigin;
this.child = info.child;
if (process.env.NODE_ENV !== 'production') {
log('Child: Registering API');
log('Child: Awaiting messages...');
}
this.child.addEventListener('message', (e) => {
if (!sanitize(e, this.parentOrigin))
return;
if (process.env.NODE_ENV !== 'production') {
log('Child: Received request', e.data);
}
const { property, uid, data, args } = e.data;
if (e.data.postmate === 'call') {
if (property in this.model &&
typeof this.model[property] === 'function') {
this.model[property](data);
}
return;
}
// Reply to Parent
resolveValue(this.model, property, args).then((value) => {
;
e.source.postMessage({
property,
postmate: 'reply',
type: messageType,
uid,
value,
}, e.origin);
});
});
}
emit(name, data) {
if (process.env.NODE_ENV !== 'production') {
log(`Child: Emitting Event "${name}"`, data);
}
this.parent.postMessage({
postmate: 'emit',
type: messageType,
value: {
name,
data,
},
}, this.parentOrigin);
}
}
/**
* The entry point of the Parent.
*/
export class Postmate {
static debug = false; // eslint-disable-line no-undef
container;
parent;
frame;
child;
childOrigin;
url;
model;
static Model;
/**
* @param opts
*/
constructor(opts) {
this.container = opts.container;
this.url = opts.url;
this.parent = window;
this.frame = document.createElement('iframe');
if (opts.id)
this.frame.id = opts.id;
if (opts.name)
this.frame.name = opts.name;
this.frame.classList.add.apply(this.frame.classList, opts.classListArray || []);
this.container.appendChild(this.frame);
this.child = this.frame.contentWindow;
this.model = opts.model || {};
}
/**
* Begins the handshake strategy
* @param {String} url The URL to send a handshake request to
* @return {Promise} Promise that resolves when the handshake is complete
*/
sendHandshake(url) {
url = url || this.url;
const childOrigin = resolveOrigin(url);
let attempt = 0;
let responseInterval;
return new Promise((resolve, reject) => {
const reply = (e) => {
if (!sanitize(e, childOrigin))
return false;
if (e.data.postmate === 'handshake-reply') {
clearInterval(responseInterval);
if (process.env.NODE_ENV !== 'production') {
log('Parent: Received handshake reply from Child');
}
this.parent.removeEventListener('message', reply, false);
this.childOrigin = e.origin;
if (process.env.NODE_ENV !== 'production') {
log('Parent: Saving Child origin', this.childOrigin);
}
return resolve(new ParentAPI(this));
}
// Might need to remove since parent might be receiving different messages
// from different hosts
if (process.env.NODE_ENV !== 'production') {
log('Parent: Invalid handshake reply');
}
return reject('Failed handshake');
};
this.parent.addEventListener('message', reply, false);
const doSend = () => {
attempt++;
if (process.env.NODE_ENV !== 'production') {
log(`Parent: Sending handshake attempt ${attempt}`, { childOrigin });
}
this.child.postMessage({
postmate: 'handshake',
type: messageType,
model: this.model,
}, childOrigin);
if (attempt === maxHandshakeRequests) {
clearInterval(responseInterval);
}
};
const loaded = () => {
doSend();
responseInterval = setInterval(doSend, 500);
};
this.frame.addEventListener('load', loaded);
if (process.env.NODE_ENV !== 'production') {
log('Parent: Loading frame', { url });
}
this.frame.src = url;
});
}
destroy() {
if (process.env.NODE_ENV !== 'production') {
log('Postmate: Destroying Postmate instance');
}
this.frame.parentNode.removeChild(this.frame);
}
}
/**
* The entry point of the Child
*/
export class Model {
child;
model;
parent;
parentOrigin;
/**
* Initializes the child, model, parent, and responds to the Parents handshake
* @param {Object} model Hash of values, functions, or promises
* @return {Promise} The Promise that resolves when the handshake has been received
*/
constructor(model) {
this.child = window;
this.model = model;
this.parent = this.child.parent;
}
/**
* Responds to a handshake initiated by the Parent
* @return {Promise} Resolves an object that exposes an API for the Child
*/
sendHandshakeReply() {
return new Promise((resolve, reject) => {
const shake = (e) => {
if (!e.data.postmate) {
return;
}
if (e.data.postmate === 'handshake') {
if (process.env.NODE_ENV !== 'production') {
log('Child: Received handshake from Parent');
}
this.child.removeEventListener('message', shake, false);
if (process.env.NODE_ENV !== 'production') {
log('Child: Sending handshake reply to Parent');
}
;
e.source.postMessage({
postmate: 'handshake-reply',
type: messageType,
}, e.origin);
this.parentOrigin = e.origin;
// Extend model with the one provided by the parent
const defaults = e.data.model;
if (defaults) {
Object.keys(defaults).forEach((key) => {
this.model[key] = defaults[key];
});
if (process.env.NODE_ENV !== 'production') {
log('Child: Inherited and extended model from Parent');
}
}
if (process.env.NODE_ENV !== 'production') {
log('Child: Saving Parent origin', this.parentOrigin);
}
return resolve(new ChildAPI(this));
}
return reject('Handshake Reply Failed');
};
this.child.addEventListener('message', shake, false);
});
}
}

Sorry, the diff of this file is too big to display