Comparing version 5.13.0 to 6.0.0
@@ -7,2 +7,3 @@ "use strict"; | ||
exports.delayedAttrRemove = delayedAttrRemove; | ||
/** | ||
@@ -14,4 +15,3 @@ * A simple remove hook generator so we remove an element after it's finished its closing animation. | ||
function delayedAttrRemove(attr, value) { | ||
var waitMs = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 500; | ||
let waitMs = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 500; | ||
return function (vnode, callback) { | ||
@@ -18,0 +18,0 @@ vnode.elm.setAttribute(attr, value); |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,11 +6,8 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.default = void 0; | ||
var _stateController = require('./state-controller'); | ||
var _stateController = _interopRequireDefault(require("./state-controller")); | ||
var _stateController2 = _interopRequireDefault(_stateController); | ||
var _stateStore = _interopRequireDefault(require("./state-store")); | ||
var _stateStore = require('./state-store'); | ||
var _stateStore2 = _interopRequireDefault(_stateStore); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -29,9 +26,9 @@ | ||
*/ | ||
exports.default = { | ||
var _default = { | ||
/** {@link StateController} class, to be subclassed by apps */ | ||
StateController: _stateController2.default, | ||
StateController: _stateController.default, | ||
/** A simple subscribable state store */ | ||
StateStore: _stateStore2.default | ||
}; | ||
StateStore: _stateStore.default | ||
}; | ||
exports.default = _default; |
@@ -6,3 +6,4 @@ "use strict"; | ||
}); | ||
exports.getNow = getNow; | ||
exports.Perf = void 0; | ||
/** | ||
@@ -12,7 +13,13 @@ * Attempt to use a high resolution timestamp when in the browswer environment, but fallback to Date.now | ||
*/ | ||
const Perf = { | ||
getNow | ||
}; | ||
exports.Perf = Perf; | ||
function getNow() { | ||
if (typeof performance !== "undefined") { | ||
if (typeof performance !== `undefined`) { | ||
return performance.now(); | ||
} | ||
return Date.now(); | ||
} |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,13 +6,8 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.default = void 0; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
var _stateStore = _interopRequireDefault(require("./state-store")); | ||
var _stateStore = require('./state-store'); | ||
var _stateStore2 = _interopRequireDefault(_stateStore); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
/** | ||
@@ -25,48 +20,37 @@ * A StateController manages state for an application or component | ||
*/ | ||
var StateController = function () { | ||
class StateController { | ||
// Create's a default store if one isn't given | ||
function StateController() { | ||
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, | ||
_ref$store = _ref.store, | ||
store = _ref$store === undefined ? null : _ref$store; | ||
constructor() { | ||
let { | ||
store = null | ||
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
this._store = store || new _stateStore.default(); | ||
_classCallCheck(this, StateController); | ||
this._store = store || new _stateStore2.default(); | ||
this._update(this.defaultState); | ||
} | ||
_createClass(StateController, [{ | ||
key: '_update', | ||
get defaultState() { | ||
return {}; | ||
} | ||
get state() { | ||
return this._store.state; | ||
} // Discourage external users from using state directly | ||
// Discourage external users from using state directly | ||
value: function _update(props) { | ||
this._store.update(props); | ||
} | ||
}, { | ||
key: 'subscribeUpdates', | ||
value: function subscribeUpdates(listener) { | ||
return this._store.subscribeUpdates(listener); | ||
} | ||
}, { | ||
key: 'unsubscribeUpdates', | ||
value: function unsubscribeUpdates(listener) { | ||
return this._store.unsubscribeUpdates(listener); | ||
} | ||
}, { | ||
key: 'defaultState', | ||
get: function get() { | ||
return {}; | ||
} | ||
}, { | ||
key: 'state', | ||
get: function get() { | ||
return this._store.state; | ||
} | ||
}]); | ||
return StateController; | ||
}(); | ||
_update(props) { | ||
this._store.update(props); | ||
} | ||
exports.default = StateController; | ||
subscribeUpdates(listener) { | ||
return this._store.subscribeUpdates(listener); | ||
} | ||
unsubscribeUpdates(listener) { | ||
return this._store.unsubscribeUpdates(listener); | ||
} | ||
} | ||
var _default = StateController; | ||
exports.default = _default; |
@@ -6,14 +6,9 @@ "use strict"; | ||
}); | ||
exports.default = void 0; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
/** | ||
* StateStore stores state and allows an observer to subscribe to state updates | ||
*/ | ||
var StateStore = function () { | ||
function StateStore() { | ||
_classCallCheck(this, StateStore); | ||
class StateStore { | ||
constructor() { | ||
this._listeners = []; | ||
@@ -23,34 +18,25 @@ this._state = {}; | ||
_createClass(StateStore, [{ | ||
key: "update", | ||
value: function update(props) { | ||
// Always create a new state object. | ||
// if lastState === newState then state hasn't changed | ||
this._state = Object.assign({}, this._state, props); | ||
this._listeners.forEach(function (listener) { | ||
return listener(props); | ||
}); | ||
} | ||
}, { | ||
key: "subscribeUpdates", | ||
value: function subscribeUpdates(listener) { | ||
this._listeners.push(listener); | ||
} | ||
}, { | ||
key: "unsubscribeUpdates", | ||
value: function unsubscribeUpdates(listener) { | ||
this._listeners = this._listeners.filter(function (l) { | ||
return l !== listener; | ||
}); | ||
} | ||
}, { | ||
key: "state", | ||
get: function get() { | ||
return this._state; | ||
} | ||
}]); | ||
get state() { | ||
return this._state; | ||
} | ||
return StateStore; | ||
}(); | ||
update(props) { | ||
// Always create a new state object. | ||
// if lastState === newState then state hasn't changed | ||
this._state = Object.assign({}, this._state, props); | ||
exports.default = StateStore; | ||
this._listeners.forEach(listener => listener(props)); | ||
} | ||
subscribeUpdates(listener) { | ||
this._listeners.push(listener); | ||
} | ||
unsubscribeUpdates(listener) { | ||
this._listeners = this._listeners.filter(l => l !== listener); | ||
} | ||
} | ||
var _default = StateStore; | ||
exports.default = _default; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,40 +6,27 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.default = void 0; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
var _cuid = _interopRequireDefault(require("cuid")); | ||
var _cuid = require('cuid'); | ||
var _webcomponent = _interopRequireDefault(require("webcomponent")); | ||
var _cuid2 = _interopRequireDefault(_cuid); | ||
var _domPatcher = require("./dom-patcher"); | ||
var _webcomponent = require('webcomponent'); | ||
var _router = _interopRequireDefault(require("./router")); | ||
var _webcomponent2 = _interopRequireDefault(_webcomponent); | ||
var hookHelpers = _interopRequireWildcard(require("./component-utils/hook-helpers")); | ||
var _domPatcher = require('./dom-patcher'); | ||
var _perf = require("./component-utils/perf"); | ||
var _router = require('./router'); | ||
var _shallowEqual = _interopRequireDefault(require("./component-utils/shallowEqual")); | ||
var _router2 = _interopRequireDefault(_router); | ||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||
var _hookHelpers = require('./component-utils/hook-helpers'); | ||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||
var hookHelpers = _interopRequireWildcard(_hookHelpers); | ||
var _perf = require('./component-utils/perf'); | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
var DOCUMENT_FRAGMENT_NODE = 11; | ||
var ATTR_TYPE_DEFAULTS = { | ||
string: '', | ||
const DOCUMENT_FRAGMENT_NODE = 11; | ||
const ATTR_TYPE_DEFAULTS = { | ||
string: ``, | ||
boolean: false, | ||
@@ -49,3 +36,4 @@ number: 0, | ||
}; | ||
var stylesheetCache = new Map(); // key is the component constructor, value is a CSSStyleSheet instance | ||
const PARAM_TYPES = new Set([Array, String, Boolean, Number, Object, Function, Map, Set]); | ||
const stylesheetCache = new Map(); // key is the component constructor, value is a CSSStyleSheet instance | ||
@@ -81,294 +69,295 @@ /** | ||
var Component = function (_WebComponent) { | ||
_inherits(Component, _WebComponent); | ||
class Component extends _webcomponent.default { | ||
/** | ||
* Defines standard component configuration. | ||
* @type {object} | ||
* @property {function} template - function transforming state object to virtual dom tree | ||
* @property {object} [helpers={}] - properties and functions injected automatically into template state object | ||
* @property {object} [routes={}] - object mapping string route expressions to handler functions | ||
* @property {object} [appState={}] - (app root component only) state object to share with nested descendant components; | ||
* if not set, root component shares entire state object with all descendants | ||
* @property {object} [defaultState={}] - default entries for component state | ||
* @property {object} [hooks={}] - extra rendering/lifecycle callbacks | ||
* @property {function} [hooks.preUpdate] - called before an update is applied | ||
* @property {function} [hooks.postUpdate] - called after an update is applied | ||
* @property {boolean} [updateSync=false] - whether to apply updates to DOM | ||
* immediately, instead of batching to one update per frame | ||
* @property {boolean} [useShadowDom=false] - whether to use Shadow DOM | ||
* @property {string} [css=''] - component-specific Shadow DOM stylesheet | ||
* @example | ||
* get config() { | ||
* return { | ||
* template: state => h('.name', `My name is ${name}`), | ||
* routes: { | ||
* 'wombat/:wombatId': (stateUpdate={}, wombatId) => { | ||
* // route handler implementation | ||
* }, | ||
* }, | ||
* }; | ||
* } | ||
*/ | ||
get config() { | ||
return {}; | ||
} | ||
/** | ||
* Template helper functions defined in config object, and exposed to template code | ||
* as $helpers. This getter uses the component's internal config cache. | ||
* @type {object} | ||
* @example | ||
* { | ||
* myHelper: () => 'some return value', | ||
* } | ||
*/ | ||
_createClass(Component, [{ | ||
key: 'child', | ||
get helpers() { | ||
return this.getConfig(`helpers`); | ||
} | ||
/** | ||
* For use inside view templates, to create a child Panel component nested under this | ||
* component, which will share its state object and update cycle. | ||
* @param {string} tagName - the HTML element tag name of the custom element | ||
* to be created | ||
* @param {object} [config={}] - snabbdom node config (second argument of h()) | ||
* @returns {object} snabbdom vnode | ||
* @example | ||
* {template: state => h('.header', this.child('my-child-widget'))} | ||
*/ | ||
/** | ||
* For use inside view templates, to create a child Panel component nested under this | ||
* component, which will share its state object and update cycle. | ||
* @param {string} tagName - the HTML element tag name of the custom element | ||
* to be created | ||
* @param {object} [config={}] - snabbdom node config (second argument of h()) | ||
* @returns {object} snabbdom vnode | ||
* @example | ||
* {template: state => h('.header', this.child('my-child-widget'))} | ||
*/ | ||
value: function child(tagName) { | ||
var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
config.props = Object.assign({}, config.props, { | ||
$panelParentID: this.panelID | ||
}); | ||
return (0, _domPatcher.h)(tagName, config); | ||
} | ||
child(tagName) { | ||
let config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
config.props = Object.assign({}, config.props, { | ||
$panelParentID: this.panelID | ||
}); | ||
return (0, _domPatcher.h)(tagName, config); | ||
} | ||
/** | ||
* Searches the component's Panel ancestors for the first component of the | ||
* given type (HTML tag name). | ||
* @param {string} tagName - tag name of the parent to search for | ||
* @returns {object} Panel component | ||
* @throws Throws an error if no parent component with the given tag name is found. | ||
* @example | ||
* myWidget.findPanelParentByTagName('my-app'); | ||
*/ | ||
/** | ||
* Searches the component's Panel ancestors for the first component of the | ||
* given type (HTML tag name). | ||
* @param {string} tagName - tag name of the parent to search for | ||
* @returns {object} Panel component | ||
* @throws Throws an error if no parent component with the given tag name is found. | ||
* @example | ||
* myWidget.findPanelParentByTagName('my-app'); | ||
*/ | ||
}, { | ||
key: 'findPanelParentByTagName', | ||
value: function findPanelParentByTagName(tagName) { | ||
tagName = tagName.toLowerCase(); | ||
for (var node = this.$panelParent; node; node = node.$panelParent) { | ||
if (node.tagName.toLowerCase() === tagName) { | ||
return node; | ||
} | ||
findPanelParentByTagName(tagName) { | ||
tagName = tagName.toLowerCase(); | ||
for (let node = this.$panelParent; node; node = node.$panelParent) { | ||
if (node.tagName.toLowerCase() === tagName) { | ||
return node; | ||
} | ||
throw Error(tagName + ' not found'); | ||
} | ||
/** | ||
* Fetches a value from the component's configuration map (a combination of | ||
* values supplied in the config() getter and defaults applied automatically). | ||
* @param {string} key - key of config item to fetch | ||
* @returns value associated with key | ||
* @example | ||
* myWidget.getConfig('css'); | ||
*/ | ||
throw Error(`${tagName} not found`); | ||
} | ||
/** | ||
* Fetches a value from the component's configuration map (a combination of | ||
* values supplied in the config() getter and defaults applied automatically). | ||
* @param {string} key - key of config item to fetch | ||
* @returns value associated with key | ||
* @example | ||
* myWidget.getConfig('css'); | ||
*/ | ||
}, { | ||
key: 'getConfig', | ||
value: function getConfig(key) { | ||
return this._config[key]; | ||
} | ||
/** | ||
* Executes the route handler matching the given URL fragment, and updates | ||
* the URL, as though the user had navigated explicitly to that address. | ||
* @param {string} fragment - URL fragment to navigate to | ||
* @param {object} [stateUpdate={}] - update to apply to state object when | ||
* routing | ||
* @example | ||
* myApp.navigate('wombat/54', {color: 'blue'}); | ||
*/ | ||
getConfig(key) { | ||
return this._config[key]; | ||
} | ||
/** | ||
* Executes the route handler matching the given URL fragment, and updates | ||
* the URL, as though the user had navigated explicitly to that address. | ||
* @param {string} fragment - URL fragment to navigate to | ||
* @param {object} [stateUpdate={}] - update to apply to state object when | ||
* routing | ||
* @example | ||
* myApp.navigate('wombat/54', {color: 'blue'}); | ||
*/ | ||
}, { | ||
key: 'navigate', | ||
value: function navigate() { | ||
var _$panelRoot$router; | ||
(_$panelRoot$router = this.$panelRoot.router).navigate.apply(_$panelRoot$router, arguments); | ||
} | ||
navigate() { | ||
this.$panelRoot.router.navigate(...arguments); | ||
} | ||
/** | ||
* Helper function which will queue a function to be run once the component has been | ||
* initialized and added to the DOM. If the component has already had its connectedCallback | ||
* run, the function will run immediately. | ||
* | ||
* It can optionally return a function to be enqueued to be run just before the component is | ||
* removed from the DOM. This occurs during the disconnectedCallback lifecycle. | ||
* @param {function} fn - callback to be run after the component has been added to the DOM. If this | ||
* callback returns another function, the returned function will be run when the component disconnects from the DOM. | ||
* @example | ||
* myApp.onConnected(() => { | ||
* const handleResize = () => calculateSize(); | ||
* document.body.addEventListener(`resize`, handleResize); | ||
* return () => document.body.removeEventListener(`resize`, handleResize); | ||
* }); | ||
*/ | ||
/** | ||
* Helper function which will queue a function to be run once the component has been | ||
* initialized and added to the DOM. If the component has already had its connectedCallback | ||
* run, the function will run immediately. | ||
* | ||
* It can optionally return a function to be enqueued to be run just before the component is | ||
* removed from the DOM. This occurs during the disconnectedCallback lifecycle. | ||
* @param {function} fn - callback to be run after the component has been added to the DOM. If this | ||
* callback returns another function, the returned function will be run when the component disconnects from the DOM. | ||
* @example | ||
* myApp.onConnected(() => { | ||
* const handleResize = () => calculateSize(); | ||
* document.body.addEventListener(`resize`, handleResize); | ||
* return () => document.body.removeEventListener(`resize`, handleResize); | ||
* }); | ||
*/ | ||
}, { | ||
key: 'onConnected', | ||
value: function onConnected(fn) { | ||
if (this.initialized) { | ||
this._maybeEnqueueResult(fn.call(this)); | ||
} | ||
this._connectedQueue.push(fn); | ||
onConnected(fn) { | ||
if (this.initialized) { | ||
this._maybeEnqueueResult(fn.call(this)); | ||
} | ||
}, { | ||
key: '_maybeEnqueueResult', | ||
value: function _maybeEnqueueResult(result) { | ||
if (result && typeof result === 'function') { | ||
result.removeAfterExec = true; | ||
this._disconnectedQueue.push(result); | ||
} | ||
} | ||
/** | ||
* Helper function which will queue a function to be run just before the component is | ||
* removed from the DOM. This occurs during the disconnectedCallback lifecycle. | ||
* | ||
* @param {function} fn - callback to be run just before the component is removed from the DOM | ||
* @example | ||
* connectedCallback() { | ||
* const shiftKeyListener = () => { | ||
* if (ev.keyCode === SHIFT_KEY_CODE) { | ||
* const doingRangeSelect = ev.type === `keydown` && this.isMouseOver && this.lastSelectedRowIdx !== null; | ||
* if (this.state.doingRangeSelect !== doingRangeSelect) { | ||
* this.update({doingRangeSelect}); | ||
* } | ||
* } | ||
* } | ||
* document.body.addEventListener(`keydown`, shiftKeyListener); | ||
* this.onDisconnected(() => { | ||
* document.body.removeEventListener(`keydown`, shiftKeyListener); | ||
* }); | ||
* } | ||
*/ | ||
this._connectedQueue.push(fn); | ||
} | ||
}, { | ||
key: 'onDisconnected', | ||
value: function onDisconnected(fn) { | ||
this._disconnectedQueue.push(fn); | ||
} | ||
_maybeEnqueueResult(result) { | ||
if (result && typeof result === `function`) { | ||
result.removeAfterExec = true; | ||
/** | ||
* Sets a value in the component's configuration map after element | ||
* initialization. | ||
* @param {string} key - key of config item to set | ||
* @param val - value to associate with key | ||
* @example | ||
* myWidget.setConfig('template', () => h('.new-template', 'Hi')); | ||
*/ | ||
}, { | ||
key: 'setConfig', | ||
value: function setConfig(key, val) { | ||
this._config[key] = val; | ||
this._disconnectedQueue.push(result); | ||
} | ||
} | ||
/** | ||
* Helper function which will queue a function to be run just before the component is | ||
* removed from the DOM. This occurs during the disconnectedCallback lifecycle. | ||
* | ||
* @param {function} fn - callback to be run just before the component is removed from the DOM | ||
* @example | ||
* connectedCallback() { | ||
* const shiftKeyListener = () => { | ||
* if (ev.keyCode === SHIFT_KEY_CODE) { | ||
* const doingRangeSelect = ev.type === `keydown` && this.isMouseOver && this.lastSelectedRowIdx !== null; | ||
* if (this.state.doingRangeSelect !== doingRangeSelect) { | ||
* this.update({doingRangeSelect}); | ||
* } | ||
* } | ||
* } | ||
* document.body.addEventListener(`keydown`, shiftKeyListener); | ||
* this.onDisconnected(() => { | ||
* document.body.removeEventListener(`keydown`, shiftKeyListener); | ||
* }); | ||
* } | ||
*/ | ||
/** | ||
* To be overridden by subclasses, defining conditional logic for whether | ||
* a component should rerender its template given the state to be applied. | ||
* In most cases this method can be left untouched, but can provide improved | ||
* performance when dealing with very many DOM elements. | ||
* @param {object} state - state object to be used when rendering | ||
* @returns {boolean} whether or not to render/update this component | ||
* @example | ||
* shouldUpdate(state) { | ||
* // don't need to rerender if result set ID hasn't changed | ||
* return state.largeResultSetID !== this._cachedResultID; | ||
* } | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
}, { | ||
key: 'shouldUpdate', | ||
value: function shouldUpdate(state) { | ||
return true; | ||
} | ||
onDisconnected(fn) { | ||
this._disconnectedQueue.push(fn); | ||
} | ||
/** | ||
* Sets a value in the component's configuration map after element | ||
* initialization. | ||
* @param {string} key - key of config item to set | ||
* @param val - value to associate with key | ||
* @example | ||
* myWidget.setConfig('template', () => h('.new-template', 'Hi')); | ||
*/ | ||
/** | ||
* Applies a state update, triggering a re-render check of the component as | ||
* well as any other components sharing the same state. This is the primary | ||
* means of updating the DOM in a Panel application. | ||
* @param {object|function} [stateUpdate={}] - keys and values of entries to update in | ||
* the component's state object | ||
* @example | ||
* myWidget.update({name: 'Bob'}); | ||
*/ | ||
}, { | ||
key: 'update', | ||
value: function update() { | ||
var stateUpdate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
setConfig(key, val) { | ||
this._config[key] = val; | ||
} | ||
/** | ||
* To be overridden by subclasses, defining conditional logic for whether | ||
* a component should rerender its template given the state to be applied. | ||
* In most cases this method can be left untouched, but can provide improved | ||
* performance when dealing with very many DOM elements. | ||
* | ||
* @deprecated use shouldComponentUpdate instead | ||
* @param {object} state - state object to be used when rendering | ||
* @returns {boolean} whether or not to render/update this component | ||
* @example | ||
* shouldUpdate(state) { | ||
* // don't need to rerender if result set ID hasn't changed | ||
* return state.largeResultSetID !== this._cachedResultID; | ||
* } | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
this.timings.lastUpdateAt = (0, _perf.getNow)(); | ||
var stateUpdateResult = typeof stateUpdate === 'function' ? stateUpdate(this.state) : stateUpdate; | ||
return this._updateStore(stateUpdateResult, { | ||
store: 'state', | ||
cascade: this.isStateShared | ||
}); | ||
} | ||
shouldUpdate(state) { | ||
return true; | ||
} | ||
/** | ||
* | ||
* Same API as react's `shouldComponentUpdate` usage | ||
* if child component implements this method, parent implmentation wil be discarded | ||
* NOTE: never call `super` in child `shouldComponentUpdate` | ||
* | ||
* there a slight difference with react: `params` or `state` could sometimes be null indicating that | ||
* the update is not related to `params` or `state` | ||
* | ||
* @param {object} params - new params object to be used when rendering | ||
* @param {object} state - state object to be used when rendering | ||
* @return {boolean} | ||
* @example | ||
* shouldComponentUpdate(params, state) { | ||
* if (params.bookmark.id === this.params.bookmark.id) { | ||
* return false; | ||
* } | ||
* return !shallowEqual(params, this.params); | ||
* } | ||
*/ | ||
/** | ||
* Applies a state update specifically to app state shared across components. | ||
* In apps which don't specify `appState` in the root component config, all | ||
* state is shared across all parent and child components and the standard | ||
* update() method should be used instead. | ||
* @param {object} [stateUpdate={}] - keys and values of entries to update in | ||
* the app's appState object | ||
* @example | ||
* myWidget.updateApp({name: 'Bob'}); | ||
*/ | ||
}, { | ||
key: 'updateApp', | ||
value: function updateApp() { | ||
var stateUpdate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return this._updateStore(stateUpdate, { store: 'appState', cascade: true }); | ||
shouldComponentUpdate(params, state) { | ||
if (params) { | ||
return !(0, _shallowEqual.default)(params, this.params); | ||
} | ||
}, { | ||
key: 'config', | ||
/** | ||
* Defines standard component configuration. | ||
* @type {object} | ||
* @property {function} template - function transforming state object to virtual dom tree | ||
* @property {object} [helpers={}] - properties and functions injected automatically into template state object | ||
* @property {object} [routes={}] - object mapping string route expressions to handler functions | ||
* @property {object} [appState={}] - (app root component only) state object to share with nested descendant components; | ||
* if not set, root component shares entire state object with all descendants | ||
* @property {object} [defaultState={}] - default entries for component state | ||
* @property {object} [hooks={}] - extra rendering/lifecycle callbacks | ||
* @property {function} [hooks.preUpdate] - called before an update is applied | ||
* @property {function} [hooks.postUpdate] - called after an update is applied | ||
* @property {boolean} [updateSync=false] - whether to apply updates to DOM | ||
* immediately, instead of batching to one update per frame | ||
* @property {boolean} [useShadowDom=false] - whether to use Shadow DOM | ||
* @property {string} [css=''] - component-specific Shadow DOM stylesheet | ||
* @example | ||
* get config() { | ||
* return { | ||
* template: state => h('.name', `My name is ${name}`), | ||
* routes: { | ||
* 'wombat/:wombatId': (stateUpdate={}, wombatId) => { | ||
* // route handler implementation | ||
* }, | ||
* }, | ||
* }; | ||
* } | ||
*/ | ||
get: function get() { | ||
return {}; | ||
} | ||
return this.shouldUpdate(state); | ||
} | ||
/** | ||
* Applies a state update, triggering a re-render check of the component as | ||
* well as any other components sharing the same state. This is the primary | ||
* means of updating the DOM in a Panel application. | ||
* @param {object|function} [stateUpdate={}] - keys and values of entries to update in | ||
* the component's state object | ||
* @example | ||
* myWidget.update({name: 'Bob'}); | ||
*/ | ||
/** | ||
* Template helper functions defined in config object, and exposed to template code | ||
* as $helpers. This getter uses the component's internal config cache. | ||
* @type {object} | ||
* @example | ||
* { | ||
* myHelper: () => 'some return value', | ||
* } | ||
*/ | ||
}, { | ||
key: 'helpers', | ||
get: function get() { | ||
return this.getConfig('helpers'); | ||
} | ||
}]); | ||
update() { | ||
let stateUpdate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
this.timings.lastUpdateAt = _perf.Perf.getNow(); | ||
const stateUpdateResult = typeof stateUpdate === `function` ? stateUpdate(this.state) : stateUpdate; | ||
return this._updateStore(stateUpdateResult, { | ||
store: `state`, | ||
cascade: this.isStateShared | ||
}); | ||
} | ||
/** | ||
* Applies a state update specifically to app state shared across components. | ||
* In apps which don't specify `appState` in the root component config, all | ||
* state is shared across all parent and child components and the standard | ||
* update() method should be used instead. | ||
* @param {object} [stateUpdate={}] - keys and values of entries to update in | ||
* the app's appState object | ||
* @example | ||
* myWidget.updateApp({name: 'Bob'}); | ||
*/ | ||
function Component() { | ||
_classCallCheck(this, Component); | ||
var _this = _possibleConstructorReturn(this, (Component.__proto__ || Object.getPrototypeOf(Component)).call(this)); | ||
updateApp() { | ||
let stateUpdate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return this._updateStore(stateUpdate, { | ||
store: `appState`, | ||
cascade: true | ||
}); | ||
} | ||
_this.timings = { | ||
createdAt: (0, _perf.getNow)() | ||
constructor() { | ||
super(); | ||
this.timings = { | ||
createdAt: _perf.Perf.getNow() | ||
}; | ||
this.panelID = (0, _cuid.default)(); | ||
this._connectedQueue = []; | ||
this._disconnectedQueue = []; | ||
this._attrs = {}; | ||
_this.panelID = (0, _cuid2.default)(); | ||
this._syncAttrs(); // constructor sync ensures default properties are present on this._attrs | ||
_this._connectedQueue = []; | ||
_this._disconnectedQueue = []; | ||
_this._attrs = {}; | ||
_this._syncAttrs(); // constructor sync ensures default properties are present on this._attrs | ||
_this._config = Object.assign({}, { | ||
css: '', | ||
this._config = Object.assign({}, { | ||
css: ``, | ||
params: {}, | ||
defaultParams: {}, | ||
defaultContexts: {}, | ||
@@ -378,4 +367,4 @@ contexts: [], | ||
routes: {}, | ||
template: function template() { | ||
throw Error('No template provided by Component subclass'); | ||
template: () => { | ||
throw Error(`No template provided by Component subclass`); | ||
}, | ||
@@ -385,49 +374,55 @@ updateSync: false, | ||
slowThreshold: 20 | ||
}, _this.config); | ||
}, this.config); | ||
_this._contexts = new Set(_this.getConfig('contexts')); | ||
this._initializeParams(); | ||
// initialize shared state store, either in `appState` or default to `state` | ||
this._contexts = new Set(this.getConfig(`contexts`)); // initialize shared state store, either in `appState` or default to `state` | ||
// appState and isStateShared of child components will be overwritten by parent/root | ||
// when the component is connected to the hierarchy | ||
_this.state = Object.assign({}, _this.getConfig('defaultState')); | ||
_this.appState = _this.getConfig('appState'); | ||
if (!_this.appState) { | ||
_this.appState = {}; | ||
_this.isStateShared = true; | ||
this.state = Object.assign({}, this.getConfig(`defaultState`)); | ||
this.appState = this.getConfig(`appState`); | ||
if (!this.appState) { | ||
this.appState = {}; | ||
this.isStateShared = true; | ||
} else { | ||
_this.isStateShared = false; | ||
this.isStateShared = false; | ||
} | ||
if (_this.getConfig('useShadowDom')) { | ||
_this.el = _this.attachShadow({ mode: 'open' }); | ||
_this.applyStaticStyle(_this.getConfig('css')); | ||
} else if (_this.getConfig('css')) { | ||
throw Error('"useShadowDom" config option must be set in order to use "css" config.'); | ||
if (this.getConfig(`useShadowDom`)) { | ||
this.el = this.attachShadow({ | ||
mode: `open` | ||
}); | ||
this.applyStaticStyle(this.getConfig(`css`)); | ||
} else if (this.getConfig(`css`)) { | ||
throw Error(`"useShadowDom" config option must be set in order to use "css" config.`); | ||
} else { | ||
_this.el = _this; | ||
this.el = this; | ||
} | ||
_this.postRenderCallback = function (elapsedMs) { | ||
_this.timings.lastRenderAt = (0, _perf.getNow)(); | ||
if (elapsedMs > _this.getConfig('slowThreshold')) { | ||
var shouldBroadcast = !_this.lastSlowRender || // SHOULD because we've never slow rendered | ||
_this.lastSlowRender.time - (0, _perf.getNow)() > 3000 || // SHOULD because last time was more than three seconds ago | ||
elapsedMs > (_this.slowestRenderMs || 0); // SHOULD because this time is slower | ||
this.postRenderCallback = elapsedMs => { | ||
this.timings.lastRenderAt = _perf.Perf.getNow(); | ||
if (elapsedMs > this.getConfig(`slowThreshold`)) { | ||
const shouldBroadcast = !this.lastSlowRender || // SHOULD because we've never slow rendered | ||
this.lastSlowRender.time - _perf.Perf.getNow() > 3000 || // SHOULD because last time was more than three seconds ago | ||
elapsedMs > (this.slowestRenderMs || 0); // SHOULD because this time is slower | ||
if (shouldBroadcast) { | ||
var comparedToLast = _this.lastSlowRender ? { | ||
const comparedToLast = this.lastSlowRender ? { | ||
// bit of a hack to get the number to only 2 digits of precision | ||
comparedToLast: +((elapsedMs - _this.lastSlowRender.elapsedMs) / _this.lastSlowRender.elapsedMs).toFixed(2), | ||
comparedToSlowest: +((elapsedMs - _this.slowestRenderMs) / _this.slowestRenderMs).toFixed(2) | ||
comparedToLast: +((elapsedMs - this.lastSlowRender.elapsedMs) / this.lastSlowRender.elapsedMs).toFixed(2), | ||
comparedToSlowest: +((elapsedMs - this.slowestRenderMs) / this.slowestRenderMs).toFixed(2) | ||
} : undefined; | ||
_this.lastSlowRender = { | ||
time: (0, _perf.getNow)(), | ||
elapsedMs: elapsedMs | ||
this.lastSlowRender = { | ||
time: _perf.Perf.getNow(), | ||
elapsedMs | ||
}; | ||
_this.slowestRenderMs = Math.max(_this.slowestRenderMs || 0, elapsedMs); | ||
_this.dispatchEvent(new CustomEvent('slowRender', { | ||
detail: Object.assign(comparedToLast || {}, { elapsedMs: elapsedMs, component: _this.toString() }), | ||
this.slowestRenderMs = Math.max(this.slowestRenderMs || 0, elapsedMs); | ||
this.dispatchEvent(new CustomEvent(`slowRender`, { | ||
detail: Object.assign(comparedToLast || {}, { | ||
elapsedMs, | ||
component: this.toString() | ||
}), | ||
bubbles: true, | ||
@@ -439,660 +434,626 @@ composed: true | ||
}; | ||
return _this; | ||
} | ||
_createClass(Component, [{ | ||
key: 'connectedCallback', | ||
value: function connectedCallback() { | ||
if (this.initialized) { | ||
return; | ||
} | ||
connectedCallback() { | ||
if (this.initialized) { | ||
return; | ||
} // Prevent re-entrant calls to connectedCallback. | ||
// This can happen in some (probably erroneous) cases with Firefox+polyfills. | ||
// Prevent re-entrant calls to connectedCallback. | ||
// This can happen in some (probably erroneous) cases with Firefox+polyfills. | ||
if (this.initializing) { | ||
return; | ||
} | ||
this.initializing = true; | ||
this.timings.initializingStartedAt = (0, _perf.getNow)(); | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
if (this.initializing) { | ||
return; | ||
} | ||
try { | ||
for (var _iterator = Object.keys(this._attrsSchema)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var attrsSchemaKey = _step.value; | ||
this.initializing = true; | ||
this.timings.initializingStartedAt = _perf.Perf.getNow(); | ||
if (!Object.prototype.hasOwnProperty.call(this._attrs, attrsSchemaKey) && this._attrsSchema[attrsSchemaKey].required) { | ||
throw new Error(this + ': is missing required attr \'' + attrsSchemaKey + '\''); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
for (const attrsSchemaKey of Object.keys(this._attrsSchema)) { | ||
if (!Object.prototype.hasOwnProperty.call(this._attrs, attrsSchemaKey) && this._attrsSchema[attrsSchemaKey].required) { | ||
throw new Error(`${this}: is missing required attr '${attrsSchemaKey}'`); | ||
} | ||
} | ||
this.$panelChildren = new Set(); | ||
this.$panelChildren = new Set(); | ||
if (typeof this.$panelParentID !== 'undefined') { | ||
this.isPanelChild = true; | ||
// find $panelParent | ||
for (var node = this.parentNode; node && !this.$panelParent; node = node.parentNode) { | ||
if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { | ||
// handle shadow-root | ||
node = node.host; | ||
} | ||
if (node.panelID === this.$panelParentID) { | ||
this.$panelParent = node; | ||
this.$panelRoot = node.$panelRoot; | ||
} | ||
if (typeof this.$panelParentID !== `undefined`) { | ||
this.isPanelChild = true; // find $panelParent | ||
for (let node = this.parentNode; node && !this.$panelParent; node = node.parentNode) { | ||
if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { | ||
// handle shadow-root | ||
node = node.host; | ||
} | ||
if (!this.$panelParent) { | ||
throw Error('panelParent ' + this.$panelParentID + ' not found'); | ||
} | ||
this.$panelParent.$panelChildren.add(this); | ||
// share either appState or all of state | ||
// flush any queued appState changes | ||
this.appState = Object.assign(this.$panelRoot.appState, this.appState); | ||
// if child element state is shared, point | ||
// state to parent's state object and flush any | ||
// queued state changes to the parent state | ||
this.isStateShared = this.$panelRoot.isStateShared; | ||
if (this.isStateShared) { | ||
this.state = Object.assign(this.$panelRoot.state, this.state); | ||
if (node.panelID === this.$panelParentID) { | ||
this.$panelParent = node; | ||
this.$panelRoot = node.$panelRoot; | ||
} | ||
} else { | ||
this.isPanelRoot = true; | ||
this.$panelRoot = this; | ||
this.$panelParent = null; | ||
} | ||
this.app = this.$panelRoot; | ||
Object.assign(this.state, this.getJSONAttribute('data-state'), this._stateFromAttributes()); | ||
if (Object.keys(this.getConfig('routes')).length) { | ||
this.router = new _router2.default(this, { historyMethod: this.historyMethod }); | ||
this.navigate(window.location.hash); | ||
if (!this.$panelParent) { | ||
throw Error(`panelParent ${this.$panelParentID} not found`); | ||
} | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
this.$panelParent.$panelChildren.add(this); // share either appState or all of state | ||
// flush any queued appState changes | ||
try { | ||
for (var _iterator2 = this.getConfig('contexts')[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var contextName = _step2.value; | ||
this.appState = Object.assign(this.$panelRoot.appState, this.appState); // if child element state is shared, point | ||
// state to parent's state object and flush any | ||
// queued state changes to the parent state | ||
var context = this.getContext(contextName); | ||
// Context classes can implement an optional `bindToComponent` callback that executes each time the component is connected to the DOM | ||
if (context.bindToComponent) { | ||
context.bindToComponent(this); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
this.isStateShared = this.$panelRoot.isStateShared; | ||
if (this.isStateShared) { | ||
this.state = Object.assign(this.$panelRoot.state, this.state); | ||
} | ||
} else { | ||
this.isPanelRoot = true; | ||
this.$panelRoot = this; | ||
this.$panelParent = null; | ||
} | ||
this.domPatcher = new _domPatcher.DOMPatcher(this.state, this._render.bind(this), { | ||
updateMode: this.getConfig('updateSync') ? 'sync' : 'async', | ||
postRenderCallback: this.postRenderCallback | ||
this.app = this.$panelRoot; | ||
Object.assign(this.state, this.getJSONAttribute(`data-state`), this._stateFromAttributes()); | ||
if (Object.keys(this.getConfig(`routes`)).length) { | ||
this.router = new _router.default(this, { | ||
historyMethod: this.historyMethod | ||
}); | ||
this.el.appendChild(this.domPatcher.el); | ||
this.navigate(window.location.hash); | ||
} | ||
for (var i = 0; i < this._connectedQueue.length; i++) { | ||
var connectedCallbackFn = this._connectedQueue[i]; | ||
try { | ||
this._maybeEnqueueResult(connectedCallbackFn.call(this)); | ||
} catch (err) { | ||
console.warn('error running onConnected function', err); | ||
} | ||
for (const contextName of this.getConfig(`contexts`)) { | ||
const context = this.getContext(contextName); // Context classes can implement an optional `bindToComponent` callback that executes each time the component is connected to the DOM | ||
if (context.bindToComponent) { | ||
context.bindToComponent(this); | ||
} | ||
this.initialized = true; | ||
this.initializing = false; | ||
this.timings.initializingCompletedAt = (0, _perf.getNow)(); | ||
this.dispatchEvent(new CustomEvent('componentInitialized', { | ||
detail: { | ||
elapsedMs: this.timings.initializingCompletedAt - this.timings.initializingStartedAt, | ||
component: this.toString() | ||
}, | ||
bubbles: true, | ||
composed: true | ||
})); | ||
} | ||
}, { | ||
key: 'disconnectedCallback', | ||
value: function disconnectedCallback() { | ||
var _this2 = this; | ||
if (!this.initialized) { | ||
return; | ||
} | ||
this.domPatcher = new _domPatcher.DOMPatcher(this.state, this._render.bind(this), { | ||
updateMode: this.getConfig(`updateSync`) ? `sync` : `async`, | ||
postRenderCallback: this.postRenderCallback | ||
}); | ||
this.el.appendChild(this.domPatcher.el); | ||
for (var i = 0; i < this._disconnectedQueue.length; i++) { | ||
var disconnectedCallbackFn = this._disconnectedQueue[i]; | ||
try { | ||
disconnectedCallbackFn.call(this); | ||
} catch (err) { | ||
console.warn('error running onDisconnected function', err); | ||
} | ||
for (let i = 0; i < this._connectedQueue.length; i++) { | ||
const connectedCallbackFn = this._connectedQueue[i]; | ||
try { | ||
this._maybeEnqueueResult(connectedCallbackFn.call(this)); | ||
} catch (err) { | ||
console.warn(`error running onConnected function`, err); | ||
} | ||
} | ||
this._disconnectedQueue = this._disconnectedQueue.filter(function (fn) { | ||
return !fn.removeAfterExec; | ||
}); | ||
this.initialized = true; | ||
this.initializing = false; | ||
this.timings.initializingCompletedAt = _perf.Perf.getNow(); | ||
this.dispatchEvent(new CustomEvent(`componentInitialized`, { | ||
detail: { | ||
elapsedMs: this.timings.initializingCompletedAt - this.timings.initializingStartedAt, | ||
component: this.toString() | ||
}, | ||
bubbles: true, | ||
composed: true | ||
})); | ||
} | ||
var _iteratorNormalCompletion3 = true; | ||
var _didIteratorError3 = false; | ||
var _iteratorError3 = undefined; | ||
disconnectedCallback() { | ||
if (!this.initialized) { | ||
return; | ||
} | ||
for (let i = 0; i < this._disconnectedQueue.length; i++) { | ||
const disconnectedCallbackFn = this._disconnectedQueue[i]; | ||
try { | ||
for (var _iterator3 = this.getConfig('contexts')[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { | ||
var contextName = _step3.value; | ||
var context = this.getContext(contextName); | ||
// Context classes can implement an optional `unbindFromComponent` callback that executes each time the component is disconnected from the DOM | ||
if (context.unbindFromComponent) { | ||
context.unbindFromComponent(this); | ||
} | ||
} | ||
disconnectedCallbackFn.call(this); | ||
} catch (err) { | ||
_didIteratorError3 = true; | ||
_iteratorError3 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion3 && _iterator3.return) { | ||
_iterator3.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError3) { | ||
throw _iteratorError3; | ||
} | ||
} | ||
console.warn(`error running onDisconnected function`, err); | ||
} | ||
} | ||
if (this.router) { | ||
this.router.unregisterListeners(); | ||
} | ||
this._disconnectedQueue = this._disconnectedQueue.filter(fn => !fn.removeAfterExec); | ||
if (this.$panelParent) { | ||
this.$panelParent.$panelChildren.delete(this); | ||
} | ||
for (const contextName of this.getConfig(`contexts`)) { | ||
const context = this.getContext(contextName); // Context classes can implement an optional `unbindFromComponent` callback that executes each time the component is disconnected from the DOM | ||
if (this.domPatcher) { | ||
this.el.removeChild(this.domPatcher.el); | ||
this.domPatcher.disconnect(); | ||
if (context.unbindFromComponent) { | ||
context.unbindFromComponent(this); | ||
} | ||
} | ||
this.domPatcher = null; | ||
this._rendered = null; | ||
this.initialized = false; | ||
if (this.router) { | ||
this.router.unregisterListeners(); | ||
} | ||
// if a child component is added via child() and has keys, snabbdom uses parentEl.insertBefore | ||
// which disconnects the element and immediately connects it at another position. | ||
// usually the child's disconnectedCallback is called before the parent's | ||
// but in that case the parents are removed from dom before the children | ||
// which causes a $panelParent not found exception for the grandchildren. | ||
// we clean up parent references in an async manner so we can handle that situation. | ||
Promise.resolve().then(function () { | ||
// only clear references if element hasn't been re-initialized | ||
if (!_this2.initialized) { | ||
_this2.$panelRoot = null; | ||
_this2.$panelParent = null; | ||
_this2.appState = null; | ||
_this2.app = null; | ||
} | ||
}); | ||
if (this.$panelParent) { | ||
this.$panelParent.$panelChildren.delete(this); | ||
} | ||
/** | ||
* Attributes schema that defines the component's html attributes and their types | ||
* Panel auto parses attribute changes into attrs() object and $attr template helper | ||
* | ||
* @typedef {object} AttrSchema | ||
* @prop {'string' | 'number' | 'boolean' | 'json'} type - type of the attribute | ||
* if not set, the attr parser will interpret it as 'string' | ||
* @prop {string} default - value if the attr is not defined | ||
* @prop {number} description - description of the attribute, what it does e.t.c | ||
* | ||
* @type {Object.<string, AttrSchema>} | ||
*/ | ||
if (this.domPatcher) { | ||
this.el.removeChild(this.domPatcher.el); | ||
this.domPatcher.disconnect(); | ||
} | ||
}, { | ||
key: 'attributeChangedCallback', | ||
value: function attributeChangedCallback(attr, oldVal, newVal) { | ||
this.timings.lastAttributeChangedAt = (0, _perf.getNow)(); | ||
this._updateAttr(attr); | ||
this.domPatcher = null; | ||
this._rendered = null; | ||
this.initialized = false; // if a child component is added via child() and has keys, snabbdom uses parentEl.insertBefore | ||
// which disconnects the element and immediately connects it at another position. | ||
// usually the child's disconnectedCallback is called before the parent's | ||
// but in that case the parents are removed from dom before the children | ||
// which causes a $panelParent not found exception for the grandchildren. | ||
// we clean up parent references in an async manner so we can handle that situation. | ||
if (attr === 'style-override') { | ||
this._applyStyleOverride(newVal); | ||
Promise.resolve().then(() => { | ||
// only clear references if element hasn't been re-initialized | ||
if (!this.initialized) { | ||
this.$panelRoot = null; | ||
this.$panelParent = null; | ||
this.appState = null; | ||
this.app = null; | ||
} | ||
}); | ||
} | ||
/** | ||
* Attributes schema that defines the component's html attributes and their types | ||
* Panel auto parses attribute changes into attrs() object and $attr template helper | ||
* | ||
* @typedef {object} AttrSchema | ||
* @prop {'string' | 'number' | 'boolean' | 'json'} type - type of the attribute | ||
* if not set, the attr parser will interpret it as 'string' | ||
* @prop {string} default - value if the attr is not defined | ||
* @prop {number} description - description of the attribute, what it does e.t.c | ||
* | ||
* @type {Object.<string, AttrSchema>} | ||
*/ | ||
if (this.initialized) { | ||
this.update(); | ||
} | ||
static get attrsSchema() { | ||
return {}; | ||
} | ||
static get observedAttributes() { | ||
return [`style-override`].concat(Object.keys(this.attrsSchema)); | ||
} | ||
attributeChangedCallback(attr, oldVal, newVal) { | ||
this.timings.lastAttributeChangedAt = _perf.Perf.getNow(); | ||
this._updateAttr(attr); | ||
if (attr === `style-override`) { | ||
this._applyStyleOverride(newVal); | ||
} | ||
}, { | ||
key: 'applyStaticStyle', | ||
value: function applyStaticStyle(styleSheetText) { | ||
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
_ref$ignoreCache = _ref.ignoreCache, | ||
ignoreCache = _ref$ignoreCache === undefined ? false : _ref$ignoreCache; | ||
if (styleSheetText) { | ||
if (this.el.adoptedStyleSheets) { | ||
// Attempt to cache the styles using Constructible StyleSheets if the feature is supported. | ||
// Note: this technique avoids the Flash of Unstyled Content that alternative approaches like <link> tags will encounter | ||
var componentKey = this.constructor; | ||
var cachedStyleSheet = stylesheetCache.get(componentKey); | ||
if (!cachedStyleSheet) { | ||
cachedStyleSheet = new CSSStyleSheet(); | ||
cachedStyleSheet.replaceSync(styleSheetText); | ||
stylesheetCache.set(componentKey, cachedStyleSheet); | ||
} else if (ignoreCache) { | ||
cachedStyleSheet.replaceSync(styleSheetText); | ||
} | ||
if (!this.staticStyleSheet) { | ||
this.staticStyleSheet = cachedStyleSheet; | ||
this.el.adoptedStyleSheets = [this.staticStyleSheet].concat(_toConsumableArray(this.el.adoptedStyleSheets.slice(1))); | ||
} | ||
} else { | ||
if (!this.staticStyleTag) { | ||
this.staticStyleTag = document.createElement('style'); | ||
this.el.insertBefore(this.staticStyleTag, this.el.childNodes[0] || null); | ||
} | ||
this.staticStyleTag.innerHTML = styleSheetText; | ||
if (this.initialized) { | ||
this.update(); | ||
} | ||
} | ||
applyStaticStyle(styleSheetText) { | ||
let { | ||
ignoreCache = false | ||
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
if (styleSheetText) { | ||
if (this.el.adoptedStyleSheets) { | ||
// Attempt to cache the styles using Constructible StyleSheets if the feature is supported. | ||
// Note: this technique avoids the Flash of Unstyled Content that alternative approaches like <link> tags will encounter | ||
const componentKey = this.constructor; | ||
let cachedStyleSheet = stylesheetCache.get(componentKey); | ||
if (!cachedStyleSheet) { | ||
cachedStyleSheet = new CSSStyleSheet(); | ||
cachedStyleSheet.replaceSync(styleSheetText); | ||
stylesheetCache.set(componentKey, cachedStyleSheet); | ||
} else if (ignoreCache) { | ||
cachedStyleSheet.replaceSync(styleSheetText); | ||
} | ||
if (!this.staticStyleSheet) { | ||
this.staticStyleSheet = cachedStyleSheet; | ||
this.el.adoptedStyleSheets = [this.staticStyleSheet, ...this.el.adoptedStyleSheets.slice(1)]; | ||
} | ||
} else { | ||
if (!this.staticStyleTag) { | ||
this.staticStyleTag = document.createElement(`style`); | ||
this.el.insertBefore(this.staticStyleTag, this.el.childNodes[0] || null); | ||
} | ||
this.staticStyleTag.innerHTML = styleSheetText; | ||
} | ||
} | ||
}, { | ||
key: '_applyStyleOverride', | ||
value: function _applyStyleOverride(styleOverride) { | ||
if (this.getConfig('useShadowDom')) { | ||
if (this.el.adoptedStyleSheets) { | ||
if (!this.styleOverrideStyleSheet) { | ||
this.styleOverrideStyleSheet = new CSSStyleSheet(); | ||
this.el.adoptedStyleSheets = this.el.adoptedStyleSheets.concat(this.styleOverrideStyleSheet); | ||
} | ||
this.styleOverrideStyleSheet.replaceSync(styleOverride || ''); | ||
} else { | ||
if (!this.styleOverrideTag) { | ||
this.styleOverrideTag = document.createElement('style'); | ||
this.el.appendChild(this.styleOverrideTag); | ||
} | ||
this.styleOverrideTag.innerHTML = styleOverride || ''; | ||
} | ||
_applyStyleOverride(styleOverride) { | ||
if (this.getConfig(`useShadowDom`)) { | ||
if (this.el.adoptedStyleSheets) { | ||
if (!this.styleOverrideStyleSheet) { | ||
this.styleOverrideStyleSheet = new CSSStyleSheet(); | ||
this.el.adoptedStyleSheets = this.el.adoptedStyleSheets.concat(this.styleOverrideStyleSheet); | ||
} | ||
this.styleOverrideStyleSheet.replaceSync(styleOverride || ``); | ||
} else { | ||
if (!this.styleOverrideTag) { | ||
this.styleOverrideTag = document.createElement(`style`); | ||
this.el.appendChild(this.styleOverrideTag); | ||
} | ||
this.styleOverrideTag.innerHTML = styleOverride || ``; | ||
} | ||
} | ||
}, { | ||
key: '_logError', | ||
value: function _logError() { | ||
var _console; | ||
} | ||
(_console = console).error.apply(_console, arguments); | ||
_logError() { | ||
console.error(...arguments); | ||
} | ||
toString() { | ||
try { | ||
return `${(this.tagName || ``).toLowerCase()}#${this.panelID}`; | ||
} catch (e) { | ||
return `UNKNOWN COMPONENT`; | ||
} | ||
}, { | ||
key: 'toString', | ||
value: function toString() { | ||
} | ||
_render(state) { | ||
if (this.shouldComponentUpdate(null, state)) { | ||
try { | ||
return (this.tagName || '').toLowerCase() + '#' + this.panelID; | ||
} catch (e) { | ||
return 'UNKNOWN COMPONENT'; | ||
this._rendered = this.getConfig(`template`).call(this, Object.assign({}, state, { | ||
$app: this.appState, | ||
$component: this, | ||
$helpers: this.helpers, | ||
$attr: this.attr.bind(this), | ||
$hooks: hookHelpers | ||
})); | ||
} catch (error) { | ||
this._logError(`Error while rendering`, this, `\n`, error); | ||
this.dispatchEvent(new CustomEvent(`renderError`, { | ||
detail: { | ||
error, | ||
component: this | ||
}, | ||
bubbles: true, | ||
composed: true | ||
})); | ||
} | ||
} | ||
}, { | ||
key: '_render', | ||
value: function _render(state) { | ||
if (this.shouldUpdate(state)) { | ||
try { | ||
this._rendered = this.getConfig('template').call(this, Object.assign({}, state, { | ||
$app: this.appState, | ||
$component: this, | ||
$helpers: this.helpers, | ||
$attr: this.attr.bind(this), | ||
$hooks: hookHelpers | ||
})); | ||
} catch (error) { | ||
this._logError('Error while rendering', this, '\n', error); | ||
this.dispatchEvent(new CustomEvent('renderError', { | ||
detail: { error: error, component: this }, | ||
bubbles: true, | ||
composed: true | ||
})); | ||
return this._rendered || _domPatcher.EMPTY_DIV; | ||
} // run a user-defined hook with the given params, if configured | ||
// cascade down tree hierarchy if option is set | ||
runHook(hookName, options) { | ||
if (!this.initialized) { | ||
return; | ||
} | ||
const hook = (this.getConfig(`hooks`) || {})[hookName]; | ||
for (var _len = arguments.length, params = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { | ||
params[_key - 2] = arguments[_key]; | ||
} | ||
if (hook) { | ||
hook(...params); | ||
} | ||
if (options.cascade) { | ||
for (const child of this.$panelChildren) { | ||
if (options.exclude !== child) { | ||
child.runHook(hookName, options, ...params); | ||
} | ||
} | ||
return this._rendered || _domPatcher.EMPTY_DIV; | ||
} | ||
} | ||
// run a user-defined hook with the given params, if configured | ||
// cascade down tree hierarchy if option is set | ||
_stateFromAttributes() { | ||
const state = {}; // this.attributes is a NamedNodeMap, without normal iterators | ||
}, { | ||
key: 'runHook', | ||
value: function runHook(hookName, options) { | ||
if (!this.initialized) { | ||
return; | ||
for (let ai = 0; ai < this.attributes.length; ai++) { | ||
const attr = this.attributes[ai]; | ||
const attrMatch = attr.name.match(/^state-(.+)/); | ||
if (attrMatch) { | ||
const num = Number(attr.value); | ||
state[attrMatch[1]] = isNaN(num) ? attr.value : num; | ||
} | ||
} | ||
var hook = (this.getConfig('hooks') || {})[hookName]; | ||
return state; | ||
} | ||
/** | ||
* Validates attrsSchema and syncs element attributes defined in attrsSchema | ||
*/ | ||
for (var _len = arguments.length, params = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { | ||
params[_key - 2] = arguments[_key]; | ||
_syncAttrs() { | ||
// maintain local validated map where all schema keys are defined | ||
this._attrsSchema = {}; | ||
const attrsSchema = this.constructor.attrsSchema; | ||
for (const attr of Object.keys(attrsSchema)) { | ||
// convert type shorthand to object | ||
let attrSchema = attrsSchema[attr]; | ||
if (typeof attrSchema === `string`) { | ||
attrSchema = { | ||
type: attrSchema | ||
}; | ||
} // Ensure attr type is valid | ||
const attrType = attrSchema.type; | ||
if (!ATTR_TYPE_DEFAULTS.hasOwnProperty(attrType)) { | ||
throw new Error(`Invalid type: ${attrType} for attr: ${attr} in attrsSchema. ` + `Only (${Object.keys(ATTR_TYPE_DEFAULTS).map(v => `'${v}'`).join(` | `)}) is valid.`); | ||
} | ||
if (hook) { | ||
hook.apply(undefined, params); | ||
if (attrSchema.default && attrSchema.required) { | ||
throw new Error(`${this}: attr '${attr}' cannot have both required and default`); | ||
} | ||
if (options.cascade) { | ||
var _iteratorNormalCompletion4 = true; | ||
var _didIteratorError4 = false; | ||
var _iteratorError4 = undefined; | ||
try { | ||
for (var _iterator4 = this.$panelChildren[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { | ||
var child = _step4.value; | ||
const attrSchemaObj = { | ||
type: attrType, | ||
default: attrSchema.hasOwnProperty(`default`) ? attrSchema.default : ATTR_TYPE_DEFAULTS[attrType], | ||
required: attrSchema.hasOwnProperty(`required`) ? attrSchema.required : false | ||
}; // convert enum to a set for perf | ||
if (options.exclude !== child) { | ||
child.runHook.apply(child, [hookName, options].concat(params)); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError4 = true; | ||
_iteratorError4 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion4 && _iterator4.return) { | ||
_iterator4.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError4) { | ||
throw _iteratorError4; | ||
} | ||
} | ||
if (attrSchema.hasOwnProperty(`enum`)) { | ||
const attrEnum = attrSchema.enum; | ||
if (!Array.isArray(attrEnum)) { | ||
throw new Error(`Enum not an array for attr: ${attr}`); | ||
} | ||
const enumSet = new Set(attrEnum); | ||
enumSet.add(attrSchema.default); | ||
attrSchemaObj.enumSet = enumSet; | ||
} | ||
this._attrsSchema[attr] = attrSchemaObj; | ||
this._updateAttr(attr); // updated at end so we don't console.warn on initial sync | ||
attrSchemaObj.deprecatedMsg = attrSchema.deprecatedMsg; | ||
} | ||
}, { | ||
key: '_stateFromAttributes', | ||
value: function _stateFromAttributes() { | ||
var state = {}; | ||
// this.attributes is a NamedNodeMap, without normal iterators | ||
for (var ai = 0; ai < this.attributes.length; ai++) { | ||
var attr = this.attributes[ai]; | ||
var attrMatch = attr.name.match(/^state-(.+)/); | ||
if (attrMatch) { | ||
var num = Number(attr.value); | ||
state[attrMatch[1]] = isNaN(num) ? attr.value : num; | ||
return this._attrs; | ||
} | ||
/** | ||
* Parses html attribute using type information from attrsSchema and updates this._attrs | ||
* @param {string} attr - attribute name | ||
*/ | ||
_updateAttr(attr) { | ||
const attrsSchema = this._attrsSchema; | ||
if (attrsSchema.hasOwnProperty(attr)) { | ||
const attrSchema = attrsSchema[attr]; | ||
const attrType = attrSchema.type; | ||
let attrValue = null; | ||
if (attrSchema.deprecatedMsg) { | ||
console.warn(`${this}: attr '${attr}' is deprecated. ${attrSchema.deprecatedMsg}`); | ||
} | ||
if (!this.hasAttribute(attr)) { | ||
if (attrType === `boolean` && (attrSchema.default || attrSchema.required)) { | ||
throw new Error(`${this}: boolean attr '${attr}' cannot have required or default, since its value is derived from whether dom element has the attribute, not its value`); | ||
} | ||
if (attrSchema.required) { | ||
// Early return because a required attribute has no explicit value | ||
return; | ||
} | ||
attrValue = attrSchema.default; | ||
} else if (attrType === `string`) { | ||
attrValue = this.getAttribute(attr); | ||
const enumSet = attrSchema.enumSet; | ||
if (enumSet && !enumSet.has(attrValue)) { | ||
throw new Error(`Invalid value: '${attrValue}' for attr: ${attr}. ` + `Only (${Array.from(enumSet).map(v => `'${v}'`).join(` | `)}) is valid.`); | ||
} | ||
} else if (attrType === `boolean`) { | ||
attrValue = this.isAttributeEnabled(attr); | ||
} else if (attrType === `number`) { | ||
attrValue = this.getNumberAttribute(attr); | ||
} else if (attrType === `json`) { | ||
attrValue = this.getJSONAttribute(attr); | ||
} | ||
return state; | ||
this._attrs[attr] = attrValue; | ||
} | ||
} | ||
/** | ||
* gets the parsed value of an attribute | ||
* @param {string} attr - attribute name | ||
*/ | ||
/** | ||
* Validates attrsSchema and syncs element attributes defined in attrsSchema | ||
*/ | ||
}, { | ||
key: '_syncAttrs', | ||
value: function _syncAttrs() { | ||
// maintain local validated map where all schema keys are defined | ||
this._attrsSchema = {}; | ||
var attrsSchema = this.constructor.attrsSchema; | ||
attr(attr) { | ||
if (attr in this._attrs) { | ||
return this._attrs[attr]; | ||
} else { | ||
throw new TypeError(`${this}: attr '${attr}' is not defined in attrsSchema`); | ||
} | ||
} | ||
/** | ||
* Returns the parsed attrs as a key-value POJO | ||
* @returns {object} parsed attribute values from attrsSchema | ||
*/ | ||
var _iteratorNormalCompletion5 = true; | ||
var _didIteratorError5 = false; | ||
var _iteratorError5 = undefined; | ||
try { | ||
for (var _iterator5 = Object.keys(attrsSchema)[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { | ||
var attr = _step5.value; | ||
attrs() { | ||
return this._attrs; | ||
} | ||
/** | ||
* parse and validate config.params and create a param schema on the component | ||
*/ | ||
// convert type shorthand to object | ||
var attrSchema = attrsSchema[attr]; | ||
if (typeof attrSchema === 'string') { | ||
attrSchema = { type: attrSchema }; | ||
} | ||
// Ensure attr type is valid | ||
var attrType = attrSchema.type; | ||
if (!ATTR_TYPE_DEFAULTS.hasOwnProperty(attrType)) { | ||
throw new Error('Invalid type: ' + attrType + ' for attr: ' + attr + ' in attrsSchema. ' + ('Only (' + Object.keys(ATTR_TYPE_DEFAULTS).map(function (v) { | ||
return '\'' + v + '\''; | ||
}).join(' | ') + ') is valid.')); | ||
} | ||
_initializeParams() { | ||
// the real value for the params | ||
this._params = {}; // maintain local validated map where all schema keys are defined | ||
if (attrSchema.default && attrSchema.required) { | ||
throw new Error(this + ': attr \'' + attr + '\' cannot have both required and default'); | ||
} | ||
this._paramSchemas = {}; | ||
const paramSchemas = this.getConfig(`params`); | ||
const defaultParams = this.getConfig(`defaultParams`); | ||
var attrSchemaObj = { | ||
type: attrType, | ||
default: attrSchema.hasOwnProperty('default') ? attrSchema.default : ATTR_TYPE_DEFAULTS[attrType], | ||
required: attrSchema.hasOwnProperty('required') ? attrSchema.required : false | ||
}; | ||
for (let [paramName, paramSchema] of Object.entries(paramSchemas)) { | ||
// convert type shorthand to object | ||
if (!paramSchema.type) { | ||
paramSchema = { | ||
type: paramSchema | ||
}; | ||
} // Ensure param type is valid | ||
// convert enum to a set for perf | ||
if (attrSchema.hasOwnProperty('enum')) { | ||
var attrEnum = attrSchema.enum; | ||
if (!Array.isArray(attrEnum)) { | ||
throw new Error('Enum not an array for attr: ' + attr); | ||
} | ||
var enumSet = new Set(attrEnum); | ||
enumSet.add(attrSchema.default); | ||
attrSchemaObj.enumSet = enumSet; | ||
} | ||
const type = paramSchema.type; | ||
this._attrsSchema[attr] = attrSchemaObj; | ||
this._updateAttr(attr); | ||
// updated at end so we don't console.warn on initial sync | ||
attrSchemaObj.deprecatedMsg = attrSchema.deprecatedMsg; | ||
} | ||
} catch (err) { | ||
_didIteratorError5 = true; | ||
_iteratorError5 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion5 && _iterator5.return) { | ||
_iterator5.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError5) { | ||
throw _iteratorError5; | ||
} | ||
} | ||
if (!PARAM_TYPES.has(type)) { | ||
const typeString = typeof type === `function` ? type.name : String(type); | ||
throw new Error(`Invalid type: ${typeString} for param: ${paramName} in paramSchema. ` + `Only (${Array.from(PARAM_TYPES.keys()).map(v => `'${v.name}'`).join(` | `)}) is valid.`); | ||
} | ||
return this._attrs; | ||
const paramSchemaObj = { | ||
type, | ||
required: Boolean(paramSchema.required), | ||
default: defaultParams[paramName] | ||
}; // set default value for the params | ||
this._params[paramName] = paramSchemaObj.default; | ||
this._paramSchemas[paramName] = paramSchemaObj; | ||
} | ||
/** | ||
* Parses html attribute using type information from attrsSchema and updates this._attrs | ||
* @param {string} attr - attribute name | ||
*/ | ||
Object.freeze(this._params); | ||
return this._paramSchemas; | ||
} | ||
}, { | ||
key: '_updateAttr', | ||
value: function _updateAttr(attr) { | ||
var attrsSchema = this._attrsSchema; | ||
if (attrsSchema.hasOwnProperty(attr)) { | ||
var attrSchema = attrsSchema[attr]; | ||
var attrType = attrSchema.type; | ||
var attrValue = null; | ||
get params() { | ||
return this._params; | ||
} | ||
if (attrSchema.deprecatedMsg) { | ||
console.warn(this + ': attr \'' + attr + '\' is deprecated. ' + attrSchema.deprecatedMsg); | ||
} | ||
setParams(params) { | ||
const shouldComponentUpdate = this.shouldComponentUpdate(params, this.state); | ||
const updateOptions = { | ||
cascade: false | ||
}; // no extra params allowed if not defined in schema | ||
if (!this.hasAttribute(attr)) { | ||
if (attrType === 'boolean' && (attrSchema.default || attrSchema.required)) { | ||
throw new Error(this + ': boolean attr \'' + attr + '\' cannot have required or default, since its value is derived from whether dom element has the attribute, not its value'); | ||
} | ||
for (const paramName of Object.keys(params)) { | ||
if (!this._paramSchemas[paramName]) { | ||
throw new Error(`extra param '${paramName}' on ${this.constructor.name} is not defined in schema`); | ||
} | ||
} | ||
if (attrSchema.required) { | ||
// Early return because a required attribute has no explicit value | ||
return; | ||
} | ||
attrValue = attrSchema.default; | ||
} else if (attrType === 'string') { | ||
attrValue = this.getAttribute(attr); | ||
var enumSet = attrSchema.enumSet; | ||
for (const [paramName, paramSchema] of Object.entries(this._paramSchemas)) { | ||
// if param defined on schema and marked required, the key must be presented on the params | ||
if (!params.hasOwnProperty(paramName) && paramSchema.required) { | ||
throw new Error(`param '${paramName}' on ${this.constructor.name} is defined as required param in schema but absent on component definition`); | ||
} | ||
if (enumSet && !enumSet.has(attrValue)) { | ||
throw new Error('Invalid value: \'' + attrValue + '\' for attr: ' + attr + '. ' + ('Only (' + Array.from(enumSet).map(function (v) { | ||
return '\'' + v + '\''; | ||
}).join(' | ') + ') is valid.')); | ||
} | ||
} else if (attrType === 'boolean') { | ||
attrValue = this.isAttributeEnabled(attr); | ||
} else if (attrType === 'number') { | ||
attrValue = this.getNumberAttribute(attr); | ||
} else if (attrType === 'json') { | ||
attrValue = this.getJSONAttribute(attr); | ||
} | ||
const paramValue = params[paramName]; // set default value if undefined value passed in | ||
this._attrs[attr] = attrValue; | ||
if (paramSchema.default && paramValue === undefined) { | ||
params[paramName] = paramSchema.default; | ||
} | ||
} | ||
/** | ||
* gets the parsed value of an attribute | ||
* @param {string} attr - attribute name | ||
*/ | ||
const newParams = Object.freeze(Object.assign({}, params)); | ||
}, { | ||
key: 'attr', | ||
value: function attr(_attr) { | ||
if (_attr in this._attrs) { | ||
return this._attrs[_attr]; | ||
} else { | ||
throw new TypeError(this + ': attr \'' + _attr + '\' is not defined in attrsSchema'); | ||
} | ||
if (this.initialized && shouldComponentUpdate) { | ||
this.runHook(`preUpdate`, updateOptions, null, newParams); | ||
} | ||
/** | ||
* Returns the parsed attrs as a key-value POJO | ||
* @returns {object} parsed attribute values from attrsSchema | ||
*/ | ||
this._params = newParams; | ||
}, { | ||
key: 'attrs', | ||
value: function attrs() { | ||
return this._attrs; | ||
if (this.initialized && shouldComponentUpdate) { | ||
this.domPatcher.update(this.state); | ||
this.runHook(`postUpdate`, updateOptions, null, newParams); | ||
} | ||
} // update helpers | ||
// Update a given state store (this.state or this.appState), with option | ||
// to 'cascade' the update across other linked components | ||
// update helpers | ||
// Update a given state store (this.state or this.appState), with option | ||
// to 'cascade' the update across other linked components | ||
_updateStore(stateUpdate) { | ||
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
const { | ||
cascade, | ||
store | ||
} = options; | ||
}, { | ||
key: '_updateStore', | ||
value: function _updateStore(stateUpdate) { | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var cascade = options.cascade, | ||
store = options.store; | ||
if (!this.initialized) { | ||
// just update store without patching DOM etc | ||
Object.assign(this[store], stateUpdate); | ||
} else { | ||
// update DOM, router, descendants etc. | ||
const updateHash = `$fragment` in stateUpdate && stateUpdate.$fragment !== this[store].$fragment; | ||
const cascadeFromRoot = cascade && !this.isPanelRoot; | ||
const updateOptions = { | ||
cascade, | ||
store | ||
}; | ||
const rootOptions = { | ||
exclude: this, | ||
cascade, | ||
store | ||
}; | ||
this.runHook(`preUpdate`, updateOptions, stateUpdate); | ||
if (!this.initialized) { | ||
// just update store without patching DOM etc | ||
Object.assign(this[store], stateUpdate); | ||
} else { | ||
// update DOM, router, descendants etc. | ||
var updateHash = '$fragment' in stateUpdate && stateUpdate.$fragment !== this[store].$fragment; | ||
var cascadeFromRoot = cascade && !this.isPanelRoot; | ||
var updateOptions = { cascade: cascade, store: store }; | ||
var rootOptions = { exclude: this, cascade: cascade, store: store }; | ||
if (cascadeFromRoot) { | ||
this.$panelRoot.runHook(`preUpdate`, rootOptions, stateUpdate); | ||
} | ||
this.runHook('preUpdate', updateOptions, stateUpdate); | ||
if (cascadeFromRoot) { | ||
this.$panelRoot.runHook('preUpdate', rootOptions, stateUpdate); | ||
} | ||
this.updateSelfAndChildren(stateUpdate, updateOptions); | ||
this.updateSelfAndChildren(stateUpdate, updateOptions); | ||
if (cascadeFromRoot) { | ||
this.$panelRoot.updateSelfAndChildren(stateUpdate, rootOptions); | ||
} | ||
if (updateHash) { | ||
this.router.replaceHash(this[store].$fragment); | ||
} | ||
if (cascadeFromRoot) { | ||
this.$panelRoot.updateSelfAndChildren(stateUpdate, rootOptions); | ||
} | ||
this.runHook('postUpdate', updateOptions, stateUpdate); | ||
if (cascadeFromRoot) { | ||
this.$panelRoot.runHook('postUpdate', rootOptions, stateUpdate); | ||
} | ||
if (updateHash) { | ||
this.router.replaceHash(this[store].$fragment); | ||
} | ||
} | ||
// Apply the given update down the component hierarchy from this node, | ||
// optionally excluding one node's subtree. This is useful for applying | ||
// a full state update to one component while sending only "shared" state | ||
// updates to the app root. | ||
this.runHook(`postUpdate`, updateOptions, stateUpdate); | ||
}, { | ||
key: 'updateSelfAndChildren', | ||
value: function updateSelfAndChildren(stateUpdate) { | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
if (!this.initialized) { | ||
return; | ||
if (cascadeFromRoot) { | ||
this.$panelRoot.runHook(`postUpdate`, rootOptions, stateUpdate); | ||
} | ||
} | ||
} // Apply the given update down the component hierarchy from this node, | ||
// optionally excluding one node's subtree. This is useful for applying | ||
// a full state update to one component while sending only "shared" state | ||
// updates to the app root. | ||
var store = options.store, | ||
cascade = options.cascade; | ||
Object.assign(this[store], stateUpdate); | ||
if (store !== 'state' || this.shouldUpdate(this[store])) { | ||
this.domPatcher.update(this.state); | ||
updateSelfAndChildren(stateUpdate) { | ||
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
if (cascade) { | ||
var _iteratorNormalCompletion6 = true; | ||
var _didIteratorError6 = false; | ||
var _iteratorError6 = undefined; | ||
if (!this.initialized) { | ||
return; | ||
} | ||
try { | ||
for (var _iterator6 = this.$panelChildren[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { | ||
var child = _step6.value; | ||
const { | ||
store, | ||
cascade | ||
} = options; | ||
Object.assign(this[store], stateUpdate); | ||
if (options.exclude !== child) { | ||
child.updateSelfAndChildren(stateUpdate, options); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError6 = true; | ||
_iteratorError6 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion6 && _iterator6.return) { | ||
_iterator6.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError6) { | ||
throw _iteratorError6; | ||
} | ||
} | ||
if (store !== `state` || this.shouldComponentUpdate(null, this[store])) { | ||
this.domPatcher.update(this.state); | ||
if (cascade) { | ||
for (const child of this.$panelChildren) { | ||
if (options.exclude !== child) { | ||
child.updateSelfAndChildren(stateUpdate, options); | ||
} | ||
@@ -1102,87 +1063,78 @@ } | ||
} | ||
}, { | ||
key: '_findNearestContextAncestor', | ||
value: function _findNearestContextAncestor() { | ||
if (!this.isConnected) { | ||
throw new Error('Cannot determine context before component is connected to the DOM'); | ||
} | ||
} | ||
var node = this.parentNode; | ||
while (node) { | ||
if (node._getAvailableContexts) { | ||
return node; | ||
} | ||
if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { | ||
// handle shadow-root | ||
node = node.host; | ||
} else { | ||
node = node.parentNode; | ||
} | ||
} | ||
return null; | ||
_findNearestContextAncestor() { | ||
if (!this.isConnected) { | ||
throw new Error(`Cannot determine context before component is connected to the DOM`); | ||
} | ||
}, { | ||
key: '_findAndMergeContextsFromAncestors', | ||
value: function _findAndMergeContextsFromAncestors() { | ||
var contextAncestor = this._findNearestContextAncestor(); | ||
var defaultContexts = Object.assign({}, this.getConfig('defaultContexts')); | ||
if (contextAncestor) { | ||
// ancestor contexts must override locally defined defaults | ||
return Object.assign(defaultContexts, contextAncestor._getAvailableContexts()); | ||
let node = this.parentNode; | ||
while (node) { | ||
if (node._getAvailableContexts) { | ||
return node; | ||
} | ||
return defaultContexts; | ||
} | ||
}, { | ||
key: '_getAvailableContexts', | ||
value: function _getAvailableContexts() { | ||
if (!this._cachedContexts) { | ||
this._cachedContexts = this._findAndMergeContextsFromAncestors(); | ||
if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { | ||
// handle shadow-root | ||
node = node.host; | ||
} else { | ||
node = node.parentNode; | ||
} | ||
return this._cachedContexts; | ||
} | ||
/** | ||
* Returns the default context of the highest (ie. closest to the document root) ancestor component | ||
* that has configured a default context for the context name. If no ancestor context is found, it will | ||
* return the component's own default context. | ||
* | ||
* @param {string} contextName - name of context | ||
* @returns {object} context object | ||
*/ | ||
return null; | ||
} | ||
}, { | ||
key: 'getContext', | ||
value: function getContext(contextName) { | ||
if (!contextName) { | ||
throw new Error('@contextName is null or empty'); | ||
} | ||
_findAndMergeContextsFromAncestors() { | ||
const contextAncestor = this._findNearestContextAncestor(); | ||
if (!this._contexts.has(contextName)) { | ||
throw new Error('@contextName must be declared in the "contexts" config array'); | ||
} | ||
const defaultContexts = Object.assign({}, this.getConfig(`defaultContexts`)); | ||
var availableContexts = this._getAvailableContexts(); | ||
if (!(contextName in availableContexts)) { | ||
throw new Error('A "' + contextName + '" context is not available. Check that this component or a DOM ancestor has provided this context in its "defaultContexts" Panel config.'); | ||
} | ||
if (contextAncestor) { | ||
// ancestor contexts must override locally defined defaults | ||
return Object.assign(defaultContexts, contextAncestor._getAvailableContexts()); | ||
} | ||
return availableContexts[contextName]; | ||
return defaultContexts; | ||
} | ||
_getAvailableContexts() { | ||
if (!this._cachedContexts) { | ||
this._cachedContexts = this._findAndMergeContextsFromAncestors(); | ||
} | ||
}], [{ | ||
key: 'attrsSchema', | ||
get: function get() { | ||
return {}; | ||
return this._cachedContexts; | ||
} | ||
/** | ||
* Returns the default context of the highest (ie. closest to the document root) ancestor component | ||
* that has configured a default context for the context name. If no ancestor context is found, it will | ||
* return the component's own default context. | ||
* | ||
* @param {string} contextName - name of context | ||
* @returns {object} context object | ||
*/ | ||
getContext(contextName) { | ||
if (!contextName) { | ||
throw new Error(`@contextName is null or empty`); | ||
} | ||
}, { | ||
key: 'observedAttributes', | ||
get: function get() { | ||
return ['style-override'].concat(Object.keys(this.attrsSchema)); | ||
if (!this._contexts.has(contextName)) { | ||
throw new Error(`@contextName must be declared in the "contexts" config array`); | ||
} | ||
}]); | ||
return Component; | ||
}(_webcomponent2.default); | ||
const availableContexts = this._getAvailableContexts(); | ||
exports.default = Component; | ||
if (!(contextName in availableContexts)) { | ||
throw new Error(`A "${contextName}" context is not available. Check that this component or a DOM ancestor has provided this context in its "defaultContexts" Panel config.`); | ||
} | ||
return availableContexts[contextName]; | ||
} | ||
} | ||
var _default = Component; | ||
exports.default = _default; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,53 +6,50 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.DOMPatcher = exports.h = exports.EMPTY_DIV = undefined; | ||
exports.EMPTY_DIV = exports.DOMPatcher = void 0; | ||
Object.defineProperty(exports, "h", { | ||
enumerable: true, | ||
get: function () { | ||
return _snabbdom.h; | ||
} | ||
}); | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** | ||
* Manages Virtual DOM -> DOM rendering cycle | ||
* @module dom-patcher | ||
* @private | ||
*/ | ||
var _snabbdom = require("snabbdom"); | ||
var _snabbdom = require('snabbdom'); | ||
var _snabbdomDelayedClass = _interopRequireDefault(require("snabbdom-delayed-class")); | ||
var _snabbdomDelayedClass = require('snabbdom-delayed-class'); | ||
var _perf = require("./component-utils/perf"); | ||
var _snabbdomDelayedClass2 = _interopRequireDefault(_snabbdomDelayedClass); | ||
var _snabbdomParamsModule = require("./component-utils/snabbdom-params-module"); | ||
var _perf = require('./component-utils/perf'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
/** | ||
* Manages Virtual DOM -> DOM rendering cycle | ||
* @module dom-patcher | ||
* @private | ||
*/ | ||
const patch = (0, _snabbdom.init)([_snabbdom.datasetModule, _snabbdom.attributesModule, _snabbdom.classModule, _snabbdom.propsModule, _snabbdom.styleModule, _snabbdom.eventListenersModule, _snabbdomDelayedClass.default, _snabbdomParamsModule.paramsModule]); | ||
const EMPTY_DIV = (0, _snabbdom.h)(`div`); | ||
exports.EMPTY_DIV = EMPTY_DIV; | ||
var patch = (0, _snabbdom.init)([_snabbdom.datasetModule, _snabbdom.attributesModule, _snabbdom.classModule, _snabbdom.propsModule, _snabbdom.styleModule, _snabbdom.eventListenersModule, _snabbdomDelayedClass2.default]); | ||
var EMPTY_DIV = exports.EMPTY_DIV = (0, _snabbdom.h)('div'); | ||
exports.h = _snabbdom.h; | ||
var DOMPatcher = exports.DOMPatcher = function () { | ||
function DOMPatcher(initialState, renderFunc) { | ||
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
_classCallCheck(this, DOMPatcher); | ||
this.updateMode = options.updateMode || 'async'; | ||
class DOMPatcher { | ||
constructor(initialState, renderFunc) { | ||
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
this.updateMode = options.updateMode || `async`; | ||
this.state = Object.assign({}, initialState); | ||
this.renderFunc = renderFunc; | ||
this.vnode = this.renderFunc(this.state); | ||
this.postRenderCallback = options.postRenderCallback; | ||
this.postRenderCallback = options.postRenderCallback; // prepare root element | ||
// prepare root element | ||
var tagName = this.vnode.sel.split(/[#.]/)[0]; | ||
var classMatches = this.vnode.sel.match(/\.[^.#]+/g); | ||
var idMatch = this.vnode.sel.match(/#[^.#]+/); | ||
const tagName = this.vnode.sel.split(/[#.]/)[0]; | ||
const classMatches = this.vnode.sel.match(/\.[^.#]+/g); | ||
const idMatch = this.vnode.sel.match(/#[^.#]+/); | ||
this.el = document.createElement(tagName); | ||
if (classMatches) { | ||
this.el.className = classMatches.map(function (c) { | ||
return c.slice(1); | ||
}).join(' '); | ||
// this attribute setting ensures that svg elements behave as expected and will ensure | ||
this.el.className = classMatches.map(c => c.slice(1)).join(` `); // this attribute setting ensures that svg elements behave as expected and will ensure | ||
// compatibility with different snabbdom versions | ||
this.el.setAttribute('class', this.el.className); | ||
this.el.setAttribute(`class`, this.el.className); | ||
} | ||
if (idMatch) { | ||
@@ -63,4 +60,6 @@ this.el.id = idMatch[0].slice(1); | ||
patch(this.el, this.vnode); | ||
if (this.el === this.vnode.elm) { | ||
var insertHook = this.vnode.data.hook && this.vnode.data.hook.insert; | ||
const insertHook = this.vnode.data.hook && this.vnode.data.hook.insert; | ||
if (insertHook) { | ||
@@ -73,67 +72,67 @@ // since Snabbdom recycled our newly-created root element (this.el) rather | ||
} | ||
this.el = this.vnode.elm; | ||
} | ||
_createClass(DOMPatcher, [{ | ||
key: 'update', | ||
value: function update(newState) { | ||
var _this = this; | ||
update(newState) { | ||
if (this.rendering) { | ||
console.error(`Applying new DOM update while render is already in progress!`); | ||
} | ||
if (this.rendering) { | ||
console.error('Applying new DOM update while render is already in progress!'); | ||
} | ||
this.pendingState = newState; | ||
this.pendingState = newState; | ||
switch (this.updateMode) { | ||
case 'async': | ||
if (!this.pending) { | ||
this.pending = true; | ||
requestAnimationFrame(function () { | ||
return _this.render(); | ||
}); | ||
} | ||
break; | ||
case 'sync': | ||
this.render(); | ||
break; | ||
} | ||
} | ||
}, { | ||
key: 'render', | ||
value: function render() { | ||
// if disconnected, don't render | ||
if (!this.renderFunc) { | ||
return; | ||
} | ||
switch (this.updateMode) { | ||
case `async`: | ||
if (!this.pending) { | ||
this.pending = true; | ||
requestAnimationFrame(() => this.render()); | ||
} | ||
var startedAt = (0, _perf.getNow)(); | ||
this.rendering = true; | ||
this.pending = false; | ||
this.state = this.pendingState; | ||
var newVnode = this.renderFunc(this.state); | ||
this.rendering = false; | ||
break; | ||
patch(this.vnode, newVnode); | ||
this.vnode = newVnode; | ||
this.el = this.vnode.elm; | ||
if (this.postRenderCallback) { | ||
this.postRenderCallback((0, _perf.getNow)() - startedAt); | ||
} | ||
case `sync`: | ||
this.render(); | ||
break; | ||
} | ||
}, { | ||
key: 'disconnect', | ||
value: function disconnect() { | ||
var vnode = this.vnode; | ||
this.renderFunc = null; | ||
this.state = null; | ||
this.vnode = null; | ||
this.el = null; | ||
this.postRenderCallback = null; | ||
// patch with empty vnode to clear out listeners in tree | ||
// this ensures we don't leave dangling DetachedHTMLElements blocking GC | ||
patch(vnode, { sel: vnode.sel, key: vnode.key }); | ||
} | ||
render() { | ||
// if disconnected, don't render | ||
if (!this.renderFunc) { | ||
return; | ||
} | ||
}]); | ||
return DOMPatcher; | ||
}(); | ||
const startedAt = _perf.Perf.getNow(); | ||
this.rendering = true; | ||
this.pending = false; | ||
this.state = this.pendingState; | ||
const newVnode = this.renderFunc(this.state); | ||
this.rendering = false; | ||
patch(this.vnode, newVnode); | ||
this.vnode = newVnode; | ||
this.el = this.vnode.elm; | ||
if (this.postRenderCallback) { | ||
this.postRenderCallback(_perf.Perf.getNow() - startedAt); | ||
} | ||
} | ||
disconnect() { | ||
const vnode = this.vnode; | ||
this.renderFunc = null; | ||
this.state = null; | ||
this.vnode = null; | ||
this.el = null; | ||
this.postRenderCallback = null; // patch with empty vnode to clear out listeners in tree | ||
// this ensures we don't leave dangling DetachedHTMLElements blocking GC | ||
patch(vnode, { | ||
sel: vnode.sel, | ||
key: vnode.key | ||
}); | ||
} | ||
} | ||
exports.DOMPatcher = DOMPatcher; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,16 +6,44 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.jsx = exports.h = exports.StateStore = exports.StateController = exports.ComponentUtils = exports.Component = undefined; | ||
Object.defineProperty(exports, "Component", { | ||
enumerable: true, | ||
get: function () { | ||
return _component.default; | ||
} | ||
}); | ||
Object.defineProperty(exports, "ComponentUtils", { | ||
enumerable: true, | ||
get: function () { | ||
return _componentUtils.default; | ||
} | ||
}); | ||
exports.StateStore = exports.StateController = exports.ParamComponent = void 0; | ||
Object.defineProperty(exports, "h", { | ||
enumerable: true, | ||
get: function () { | ||
return _domPatcher.h; | ||
} | ||
}); | ||
Object.defineProperty(exports, "jsx", { | ||
enumerable: true, | ||
get: function () { | ||
return _snabbdomJsxLite.jsx; | ||
} | ||
}); | ||
Object.defineProperty(exports, "shallowEqual", { | ||
enumerable: true, | ||
get: function () { | ||
return _shallowEqual.default; | ||
} | ||
}); | ||
var _component = require('./component'); | ||
var _component = _interopRequireDefault(require("./component")); | ||
var _component2 = _interopRequireDefault(_component); | ||
var _componentUtils = _interopRequireDefault(require("./component-utils")); | ||
var _componentUtils = require('./component-utils'); | ||
var _shallowEqual = _interopRequireDefault(require("./component-utils/shallowEqual")); | ||
var _componentUtils2 = _interopRequireDefault(_componentUtils); | ||
var _domPatcher = require("./dom-patcher"); | ||
var _domPatcher = require('./dom-patcher'); | ||
var _snabbdomJsxLite = require("snabbdom-jsx-lite"); | ||
var _snabbdomJsxLite = require('snabbdom-jsx-lite'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -32,10 +60,9 @@ | ||
*/ | ||
var StateController = _componentUtils2.default.StateController, | ||
StateStore = _componentUtils2.default.StateStore; | ||
exports.Component = _component2.default; | ||
exports.ComponentUtils = _componentUtils2.default; | ||
const { | ||
StateController, | ||
StateStore | ||
} = _componentUtils.default; | ||
exports.StateStore = StateStore; | ||
exports.StateController = StateController; | ||
exports.StateStore = StateStore; | ||
exports.h = _domPatcher.h; | ||
exports.jsx = _snabbdomJsxLite.jsx; | ||
const ParamComponent = _component.default; | ||
exports.ParamComponent = ParamComponent; |
@@ -1,20 +0,11 @@ | ||
'use strict'; | ||
"use strict"; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
require("html-element/global-shim"); | ||
var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; | ||
var _raf = _interopRequireDefault(require("raf")); | ||
require('html-element/global-shim'); | ||
var _raf = require('raf'); | ||
var _raf2 = _interopRequireDefault(_raf); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
/* eslint-env node */ | ||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* eslint-env node */ | ||
/** | ||
@@ -40,73 +31,33 @@ * Node.js polyfill for rendering Panel components without a browser. | ||
*/ | ||
// make raf globally available unless a requestAnimationFrame implementation | ||
// is already there | ||
global.requestAnimationFrame = global.requestAnimationFrame || _raf2.default; | ||
global.requestAnimationFrame = global.requestAnimationFrame || _raf.default; // run a callback for every node of the DOM (sub)tree rooted at the given root node | ||
// run a callback for every node of the DOM (sub)tree rooted at the given root node | ||
function walkDomTree(root, callback) { | ||
// basic breadth-first tree traversal (non-recursive) | ||
var breadthQueue = [root]; | ||
const breadthQueue = [root]; | ||
while (breadthQueue.length > 0) { | ||
var node = breadthQueue.shift(); | ||
const node = breadthQueue.shift(); | ||
callback(node); | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
try { | ||
for (var _iterator = Array.from(node.childNodes || [])[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var _child = _step.value; | ||
breadthQueue.push(_child); | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
for (const child of Array.from(node.childNodes || [])) { | ||
breadthQueue.push(child); | ||
} | ||
if (node.shadowRoot) { | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
try { | ||
for (var _iterator2 = Array.from(node.shadowRoot.childNodes || [])[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var child = _step2.value; | ||
breadthQueue.push(child); | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
for (const child of Array.from(node.shadowRoot.childNodes || [])) { | ||
breadthQueue.push(child); | ||
} | ||
} | ||
} | ||
} | ||
} // patch DOM insertion functions to call connectedCallback on Custom Elements | ||
// patch DOM insertion functions to call connectedCallback on Custom Elements | ||
['appendChild', 'insertBefore', 'replaceChild'].forEach(function (funcName) { | ||
var origFunc = Element.prototype[funcName]; | ||
[`appendChild`, `insertBefore`, `replaceChild`].forEach(funcName => { | ||
const origFunc = Element.prototype[funcName]; | ||
Element.prototype[funcName] = function () { | ||
var child = origFunc.apply(this, arguments); | ||
const child = origFunc.apply(this, arguments); | ||
if (this.isConnected) { | ||
@@ -116,2 +67,3 @@ walkDomTree(child, function (node) { | ||
node.isConnected = true; | ||
if (node.connectedCallback) { | ||
@@ -124,8 +76,9 @@ node.connectedCallback(); | ||
}; | ||
}); | ||
}); // patch removeChild to call disconnectedCallback | ||
// patch removeChild to call disconnectedCallback | ||
var origRemoveChild = Element.prototype.removeChild; | ||
const origRemoveChild = Element.prototype.removeChild; | ||
Element.prototype.removeChild = function (child) { | ||
origRemoveChild.call(this, child); | ||
if (this.isConnected) { | ||
@@ -135,2 +88,3 @@ walkDomTree(child, function (node) { | ||
node.isConnected = false; | ||
if (node.disconnectedCallback) { | ||
@@ -142,80 +96,67 @@ node.disconnectedCallback(); | ||
} | ||
return child; | ||
}; | ||
Node.DOCUMENT_FRAGMENT_NODE = 11; | ||
Node.DOCUMENT_FRAGMENT_NODE = 11; // html-element does not provide hasAttribute | ||
// html-element does not provide hasAttribute | ||
Element.prototype.hasAttribute = function (name) { | ||
return this.getAttribute(name) !== null; | ||
}; | ||
// html-element only provides Element (with a lot of the HTMLElement API baked in). | ||
}; // html-element only provides Element (with a lot of the HTMLElement API baked in). | ||
// Use HTMLElement as our Web Components-ready extension. | ||
var HTMLElement = function (_Element) { | ||
_inherits(HTMLElement, _Element); | ||
function HTMLElement() { | ||
_classCallCheck(this, HTMLElement); | ||
class HTMLElement extends Element { | ||
setAttribute(name, value) { | ||
const oldValue = this.getAttribute(name); | ||
super.setAttribute(...arguments); | ||
return _possibleConstructorReturn(this, (HTMLElement.__proto__ || Object.getPrototypeOf(HTMLElement)).apply(this, arguments)); | ||
this.__onAttrChanged(name, oldValue, value); | ||
} | ||
_createClass(HTMLElement, [{ | ||
key: 'setAttribute', | ||
value: function setAttribute(name, value) { | ||
var oldValue = this.getAttribute(name); | ||
_get(HTMLElement.prototype.__proto__ || Object.getPrototypeOf(HTMLElement.prototype), 'setAttribute', this).apply(this, arguments); | ||
this.__onAttrChanged(name, oldValue, value); | ||
removeAttribute(name) { | ||
const oldValue = this.getAttribute(name); | ||
super.removeAttribute(...arguments); | ||
this.__onAttrChanged(name, oldValue, null); | ||
} | ||
attachShadow() { | ||
this.shadowRoot = document.createElement(`shadow-root`); | ||
this.shadowRoot.nodeType = Node.DOCUMENT_FRAGMENT_NODE; | ||
this.shadowRoot.host = this; | ||
if (this.isConnected) { | ||
this.shadowRoot.isConnected = true; | ||
} | ||
}, { | ||
key: 'removeAttribute', | ||
value: function removeAttribute(name) { | ||
var oldValue = this.getAttribute(name); | ||
_get(HTMLElement.prototype.__proto__ || Object.getPrototypeOf(HTMLElement.prototype), 'removeAttribute', this).apply(this, arguments); | ||
this.__onAttrChanged(name, oldValue, null); | ||
return this.shadowRoot; | ||
} | ||
__attrIsObserved(name) { | ||
if (!this.__observedAttrs) { | ||
this.__observedAttrs = this.constructor.observedAttributes || []; | ||
} | ||
}, { | ||
key: 'attachShadow', | ||
value: function attachShadow() { | ||
this.shadowRoot = document.createElement('shadow-root'); | ||
this.shadowRoot.nodeType = Node.DOCUMENT_FRAGMENT_NODE; | ||
this.shadowRoot.host = this; | ||
if (this.isConnected) { | ||
this.shadowRoot.isConnected = true; | ||
} | ||
return this.shadowRoot; | ||
return this.__observedAttrs.includes(name); | ||
} | ||
__onAttrChanged(name, oldValue, newValue) { | ||
if (this.attributeChangedCallback && this.__attrIsObserved(name)) { | ||
this.attributeChangedCallback && this.attributeChangedCallback(name, oldValue, newValue); | ||
} | ||
}, { | ||
key: '__attrIsObserved', | ||
value: function __attrIsObserved(name) { | ||
if (!this.__observedAttrs) { | ||
this.__observedAttrs = this.constructor.observedAttributes || []; | ||
} | ||
return this.__observedAttrs.includes(name); | ||
} | ||
}, { | ||
key: '__onAttrChanged', | ||
value: function __onAttrChanged(name, oldValue, newValue) { | ||
if (this.attributeChangedCallback && this.__attrIsObserved(name)) { | ||
this.attributeChangedCallback && this.attributeChangedCallback(name, oldValue, newValue); | ||
} | ||
} | ||
}]); | ||
} | ||
return HTMLElement; | ||
}(Element); | ||
} | ||
global.HTMLElement = HTMLElement; | ||
global.HTMLElement = HTMLElement; // Document patches for Custom Elements | ||
// Document patches for Custom Elements | ||
const customElementsRegistry = global._customElementsRegistry = global._customElementsRegistry || {}; | ||
const originalCreateElement = Document.prototype.createElement; | ||
var customElementsRegistry = global._customElementsRegistry = global._customElementsRegistry || {}; | ||
var originalCreateElement = Document.prototype.createElement; | ||
Document.prototype.createElement = function (tagName) { | ||
tagName = tagName.toLowerCase(); | ||
var customElClass = customElementsRegistry[tagName]; | ||
var el = void 0; | ||
const customElClass = customElementsRegistry[tagName]; | ||
let el; | ||
if (customElClass) { | ||
@@ -225,7 +166,9 @@ el = new customElClass(); | ||
} else { | ||
el = originalCreateElement.apply(undefined, arguments); | ||
el = originalCreateElement(...arguments); | ||
} | ||
if (tagName === 'body') { | ||
if (tagName === `body`) { | ||
el.isConnected = true; | ||
} | ||
return el; | ||
@@ -235,9 +178,11 @@ }; | ||
global.customElements = global.customElements || { | ||
get: function get(tagName) { | ||
get(tagName) { | ||
return customElementsRegistry[tagName]; | ||
}, | ||
define: function define(tagName, proto) { | ||
define(tagName, proto) { | ||
tagName = tagName.toLowerCase(); | ||
if (customElementsRegistry[tagName]) { | ||
throw new Error('Registration failed for type \'' + tagName + '\'. A type with that name is already registered.'); | ||
throw new Error(`Registration failed for type '${tagName}'. A type with that name is already registered.`); | ||
} else { | ||
@@ -247,2 +192,3 @@ customElementsRegistry[tagName] = proto; | ||
} | ||
}; |
@@ -6,13 +6,6 @@ "use strict"; | ||
}); | ||
exports.default = void 0; | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
function stripHash(fragment) { | ||
return fragment.replace(/^#*/, ""); | ||
return fragment.replace(/^#*/, ``); | ||
} | ||
@@ -25,35 +18,28 @@ | ||
return decodeURIComponent(currFragment) === decodeURIComponent(newFragment); | ||
} | ||
} // just the necessary bits of Backbone router+history | ||
// just the necessary bits of Backbone router+history | ||
var Router = function () { | ||
function Router(app) { | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
_classCallCheck(this, Router); | ||
class Router { | ||
constructor(app) { | ||
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
// allow injecting window dep | ||
this.window = options.window || window; | ||
this.app = app; | ||
var routeDefs = this.app.getConfig("routes"); | ||
// https://github.com/jashkenas/backbone/blob/d682061a/backbone.js#L1476-L1479 | ||
const routeDefs = this.app.getConfig(`routes`); // https://github.com/jashkenas/backbone/blob/d682061a/backbone.js#L1476-L1479 | ||
// Cached regular expressions for matching named param parts and splatted | ||
// parts of route strings. | ||
var optionalParam = /\((.*?)\)/g; | ||
var namedParam = /(\(\?)?:\w+/g; | ||
var splatParam = /\*\w+/g; | ||
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; // eslint-disable-line no-useless-escape | ||
this.compiledRoutes = Object.keys(routeDefs).map(function (routeExpr) { | ||
const optionalParam = /\((.*?)\)/g; | ||
const namedParam = /(\(\?)?:\w+/g; | ||
const splatParam = /\*\w+/g; | ||
const escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; // eslint-disable-line no-useless-escape | ||
this.compiledRoutes = Object.keys(routeDefs).map(routeExpr => { | ||
// https://github.com/jashkenas/backbone/blob/d682061a/backbone.js#L1537-L1547 | ||
var expr = routeExpr.replace(escapeRegExp, "\\$&").replace(optionalParam, "(?:$1)?").replace(namedParam, function (match, optional) { | ||
return optional ? match : "([^/?]+)"; | ||
}).replace(splatParam, "([^?]*?)"); | ||
expr = new RegExp("^" + expr + "(?:\\?([\\s\\S]*))?$"); | ||
let expr = routeExpr.replace(escapeRegExp, `\\$&`).replace(optionalParam, `(?:$1)?`).replace(namedParam, (match, optional) => optional ? match : `([^/?]+)`).replace(splatParam, `([^?]*?)`); | ||
expr = new RegExp(`^` + expr + `(?:\\?([\\s\\S]*))?$`); // hook up route handler function | ||
// hook up route handler function | ||
var handler = routeDefs[routeExpr]; | ||
if (typeof handler === "string") { | ||
let handler = routeDefs[routeExpr]; | ||
if (typeof handler === `string`) { | ||
// reference to another handler rather than its own function | ||
@@ -63,128 +49,99 @@ handler = routeDefs[handler]; | ||
return { expr: expr, handler: handler }; | ||
return { | ||
expr, | ||
handler | ||
}; | ||
}); | ||
this.registerListeners(options.historyMethod || "pushState"); | ||
this.registerListeners(options.historyMethod || `pushState`); | ||
} | ||
_createClass(Router, [{ | ||
key: "registerListeners", | ||
value: function registerListeners(historyMethod) { | ||
var _this = this; | ||
registerListeners(historyMethod) { | ||
var _this = this; | ||
this.navigateToHash = function () { | ||
return _this.navigate(_this.window.location.hash); | ||
}; | ||
this.window.addEventListener("popstate", this.navigateToHash); | ||
this.navigateToHash = () => this.navigate(this.window.location.hash); | ||
this.historyMethod = historyMethod; | ||
this.origChangeStateMethod = this.window.history[this.historyMethod]; | ||
this.window.addEventListener(`popstate`, this.navigateToHash); | ||
this.historyMethod = historyMethod; | ||
this.origChangeStateMethod = this.window.history[this.historyMethod]; | ||
this.window.history[this.historyMethod] = function () { | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
this.window.history[this.historyMethod] = function () { | ||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
_this.origChangeStateMethod.apply(_this.window.history, args); | ||
_this.navigateToHash(); | ||
// fire "pushstate" or "replacestate" event so external action can be taken on url change | ||
// these events are meant to be congruent with native "popstate" event | ||
_this.app.dispatchEvent(new CustomEvent(_this.historyMethod.toLowerCase())); | ||
}; | ||
} | ||
}, { | ||
key: "unregisterListeners", | ||
value: function unregisterListeners() { | ||
this.window.removeEventListener("popstate", this.navigateToHash); | ||
this.window.history[this.historyMethod] = this.origChangeStateMethod; | ||
} | ||
}, { | ||
key: "navigate", | ||
value: function navigate(fragment) { | ||
var _this2 = this; | ||
_this.origChangeStateMethod.apply(_this.window.history, args); | ||
var stateUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
_this.navigateToHash(); // fire "pushstate" or "replacestate" event so external action can be taken on url change | ||
// these events are meant to be congruent with native "popstate" event | ||
fragment = stripHash(fragment); | ||
if (decodedFragmentsEqual(this.app.state.$fragment, fragment) && !Object.keys(stateUpdate).length) { | ||
return; | ||
} | ||
stateUpdate.$fragment = fragment; | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
_this.app.dispatchEvent(new CustomEvent(_this.historyMethod.toLowerCase())); | ||
}; | ||
} | ||
try { | ||
for (var _iterator = this.compiledRoutes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var route = _step.value; | ||
unregisterListeners() { | ||
this.window.removeEventListener(`popstate`, this.navigateToHash); | ||
this.window.history[this.historyMethod] = this.origChangeStateMethod; | ||
} | ||
var matches = route.expr.exec(fragment); | ||
if (matches) { | ||
var _ret = function () { | ||
// extract params | ||
// https://github.com/jashkenas/backbone/blob/d682061a/backbone.js#L1553-L1558 | ||
var params = matches.slice(1); | ||
params = params.map(function (param, i) { | ||
// Don't decode the search params. | ||
if (i === params.length - 1) { | ||
return param || null; | ||
} | ||
return param ? decodeURIComponent(param) : null; | ||
}); | ||
navigate(fragment) { | ||
let stateUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
fragment = stripHash(fragment); | ||
var routeHandler = route.handler; | ||
if (!routeHandler) { | ||
throw "No route handler defined for #" + fragment; | ||
} | ||
var routeStateUpdate = routeHandler.call.apply(routeHandler, [_this2.app, stateUpdate].concat(_toConsumableArray(params))); | ||
if (routeStateUpdate) { | ||
// don't update if route handler returned a falsey result | ||
_this2.app.update(Object.assign({}, stateUpdate, routeStateUpdate)); | ||
} | ||
return { | ||
v: void 0 | ||
}; | ||
}(); | ||
if (decodedFragmentsEqual(this.app.state.$fragment, fragment) && !Object.keys(stateUpdate).length) { | ||
return; | ||
} | ||
if ((typeof _ret === "undefined" ? "undefined" : _typeof(_ret)) === "object") return _ret.v; | ||
stateUpdate.$fragment = fragment; | ||
for (const route of this.compiledRoutes) { | ||
const matches = route.expr.exec(fragment); | ||
if (matches) { | ||
// extract params | ||
// https://github.com/jashkenas/backbone/blob/d682061a/backbone.js#L1553-L1558 | ||
let params = matches.slice(1); | ||
params = params.map((param, i) => { | ||
// Don't decode the search params. | ||
if (i === params.length - 1) { | ||
return param || null; | ||
} | ||
return param ? decodeURIComponent(param) : null; | ||
}); | ||
const routeHandler = route.handler; | ||
if (!routeHandler) { | ||
throw `No route handler defined for #${fragment}`; | ||
} | ||
// no route matched | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
const routeStateUpdate = routeHandler.call(this.app, stateUpdate, ...params); | ||
if (routeStateUpdate) { | ||
// don't update if route handler returned a falsey result | ||
this.app.update(Object.assign({}, stateUpdate, routeStateUpdate)); | ||
} | ||
return; | ||
} | ||
} // no route matched | ||
console.error("No route found matching #" + fragment); | ||
} | ||
}, { | ||
key: "replaceHash", | ||
value: function replaceHash(fragment) { | ||
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
_ref$historyMethod = _ref.historyMethod, | ||
historyMethod = _ref$historyMethod === undefined ? null : _ref$historyMethod; | ||
historyMethod = historyMethod || this.historyMethod; | ||
fragment = stripHash(fragment); | ||
if (!decodedFragmentsEqual(stripHash(this.window.location.hash), fragment)) { | ||
this.window.history[historyMethod](null, null, "#" + fragment); | ||
} | ||
console.error(`No route found matching #${fragment}`); | ||
} | ||
replaceHash(fragment) { | ||
let { | ||
historyMethod = null | ||
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
historyMethod = historyMethod || this.historyMethod; | ||
fragment = stripHash(fragment); | ||
if (!decodedFragmentsEqual(stripHash(this.window.location.hash), fragment)) { | ||
this.window.history[historyMethod](null, null, `#${fragment}`); | ||
} | ||
}]); | ||
} | ||
return Router; | ||
}(); | ||
} | ||
exports.default = Router; |
@@ -5,3 +5,7 @@ /** | ||
*/ | ||
export function getNow() { | ||
export const Perf = { | ||
getNow, | ||
}; | ||
function getNow() { | ||
if (typeof performance !== `undefined`) { | ||
@@ -8,0 +12,0 @@ return performance.now(); |
@@ -7,3 +7,4 @@ import cuid from 'cuid'; | ||
import * as hookHelpers from './component-utils/hook-helpers'; | ||
import {getNow} from './component-utils/perf'; | ||
import {Perf} from './component-utils/perf'; | ||
import shallowEqual from './component-utils/shallowEqual'; | ||
@@ -17,2 +18,3 @@ const DOCUMENT_FRAGMENT_NODE = 11; | ||
}; | ||
const PARAM_TYPES = new Set([Array, String, Boolean, Number, Object, Function, Map, Set]); | ||
const stylesheetCache = new Map(); // key is the component constructor, value is a CSSStyleSheet instance | ||
@@ -227,2 +229,4 @@ | ||
* performance when dealing with very many DOM elements. | ||
* | ||
* @deprecated use shouldComponentUpdate instead | ||
* @param {object} state - state object to be used when rendering | ||
@@ -242,2 +246,29 @@ * @returns {boolean} whether or not to render/update this component | ||
/** | ||
* | ||
* Same API as react's `shouldComponentUpdate` usage | ||
* if child component implements this method, parent implmentation wil be discarded | ||
* NOTE: never call `super` in child `shouldComponentUpdate` | ||
* | ||
* there a slight difference with react: `params` or `state` could sometimes be null indicating that | ||
* the update is not related to `params` or `state` | ||
* | ||
* @param {object} params - new params object to be used when rendering | ||
* @param {object} state - state object to be used when rendering | ||
* @return {boolean} | ||
* @example | ||
* shouldComponentUpdate(params, state) { | ||
* if (params.bookmark.id === this.params.bookmark.id) { | ||
* return false; | ||
* } | ||
* return !shallowEqual(params, this.params); | ||
* } | ||
*/ | ||
shouldComponentUpdate(params, state) { | ||
if (params) { | ||
return !shallowEqual(params, this.params); | ||
} | ||
return this.shouldUpdate(state); | ||
} | ||
/** | ||
* Applies a state update, triggering a re-render check of the component as | ||
@@ -252,3 +283,3 @@ * well as any other components sharing the same state. This is the primary | ||
update(stateUpdate = {}) { | ||
this.timings.lastUpdateAt = getNow(); | ||
this.timings.lastUpdateAt = Perf.getNow(); | ||
@@ -279,3 +310,3 @@ const stateUpdateResult = typeof stateUpdate === `function` ? stateUpdate(this.state) : stateUpdate; | ||
this.timings = { | ||
createdAt: getNow(), | ||
createdAt: Perf.getNow(), | ||
}; | ||
@@ -295,2 +326,4 @@ | ||
css: ``, | ||
params: {}, | ||
defaultParams: {}, | ||
defaultContexts: {}, | ||
@@ -310,2 +343,4 @@ contexts: [], | ||
this._initializeParams(); | ||
this._contexts = new Set(this.getConfig(`contexts`)); | ||
@@ -336,7 +371,7 @@ | ||
this.postRenderCallback = (elapsedMs) => { | ||
this.timings.lastRenderAt = getNow(); | ||
this.timings.lastRenderAt = Perf.getNow(); | ||
if (elapsedMs > this.getConfig(`slowThreshold`)) { | ||
const shouldBroadcast = | ||
!this.lastSlowRender || // SHOULD because we've never slow rendered | ||
this.lastSlowRender.time - getNow() > 3000 || // SHOULD because last time was more than three seconds ago | ||
this.lastSlowRender.time - Perf.getNow() > 3000 || // SHOULD because last time was more than three seconds ago | ||
elapsedMs > (this.slowestRenderMs || 0); // SHOULD because this time is slower | ||
@@ -356,3 +391,3 @@ | ||
this.lastSlowRender = { | ||
time: getNow(), | ||
time: Perf.getNow(), | ||
elapsedMs, | ||
@@ -384,3 +419,3 @@ }; | ||
this.initializing = true; | ||
this.timings.initializingStartedAt = getNow(); | ||
this.timings.initializingStartedAt = Perf.getNow(); | ||
@@ -466,3 +501,3 @@ for (const attrsSchemaKey of Object.keys(this._attrsSchema)) { | ||
this.initializing = false; | ||
this.timings.initializingCompletedAt = getNow(); | ||
this.timings.initializingCompletedAt = Perf.getNow(); | ||
this.dispatchEvent( | ||
@@ -559,3 +594,3 @@ new CustomEvent(`componentInitialized`, { | ||
attributeChangedCallback(attr, oldVal, newVal) { | ||
this.timings.lastAttributeChangedAt = getNow(); | ||
this.timings.lastAttributeChangedAt = Perf.getNow(); | ||
this._updateAttr(attr); | ||
@@ -631,3 +666,3 @@ | ||
_render(state) { | ||
if (this.shouldUpdate(state)) { | ||
if (this.shouldComponentUpdate(null, state)) { | ||
try { | ||
@@ -822,2 +857,88 @@ this._rendered = this.getConfig(`template`).call( | ||
/** | ||
* parse and validate config.params and create a param schema on the component | ||
*/ | ||
_initializeParams() { | ||
// the real value for the params | ||
this._params = {}; | ||
// maintain local validated map where all schema keys are defined | ||
this._paramSchemas = {}; | ||
const paramSchemas = this.getConfig(`params`); | ||
const defaultParams = this.getConfig(`defaultParams`); | ||
for (let [paramName, paramSchema] of Object.entries(paramSchemas)) { | ||
// convert type shorthand to object | ||
if (!paramSchema.type) { | ||
paramSchema = {type: paramSchema}; | ||
} | ||
// Ensure param type is valid | ||
const type = paramSchema.type; | ||
if (!PARAM_TYPES.has(type)) { | ||
const typeString = typeof type === `function` ? type.name : String(type); | ||
throw new Error( | ||
`Invalid type: ${typeString} for param: ${paramName} in paramSchema. ` + | ||
`Only (${Array.from(PARAM_TYPES.keys()) | ||
.map((v) => `'${v.name}'`) | ||
.join(` | `)}) is valid.`, | ||
); | ||
} | ||
const paramSchemaObj = { | ||
type, | ||
required: Boolean(paramSchema.required), | ||
default: defaultParams[paramName], | ||
}; | ||
// set default value for the params | ||
this._params[paramName] = paramSchemaObj.default; | ||
this._paramSchemas[paramName] = paramSchemaObj; | ||
} | ||
Object.freeze(this._params); | ||
return this._paramSchemas; | ||
} | ||
get params() { | ||
return this._params; | ||
} | ||
setParams(params) { | ||
const shouldComponentUpdate = this.shouldComponentUpdate(params, this.state); | ||
const updateOptions = { | ||
cascade: false, | ||
}; | ||
// no extra params allowed if not defined in schema | ||
for (const paramName of Object.keys(params)) { | ||
if (!this._paramSchemas[paramName]) { | ||
throw new Error(`extra param '${paramName}' on ${this.constructor.name} is not defined in schema`); | ||
} | ||
} | ||
for (const [paramName, paramSchema] of Object.entries(this._paramSchemas)) { | ||
// if param defined on schema and marked required, the key must be presented on the params | ||
if (!params.hasOwnProperty(paramName) && paramSchema.required) { | ||
throw new Error( | ||
`param '${paramName}' on ${this.constructor.name} is defined as required param in schema but absent on component definition`, | ||
); | ||
} | ||
const paramValue = params[paramName]; | ||
// set default value if undefined value passed in | ||
if (paramSchema.default && paramValue === undefined) { | ||
params[paramName] = paramSchema.default; | ||
} | ||
} | ||
const newParams = Object.freeze(Object.assign({}, params)); | ||
if (this.initialized && shouldComponentUpdate) { | ||
this.runHook(`preUpdate`, updateOptions, null, newParams); | ||
} | ||
this._params = newParams; | ||
if (this.initialized && shouldComponentUpdate) { | ||
this.domPatcher.update(this.state); | ||
this.runHook(`postUpdate`, updateOptions, null, newParams); | ||
} | ||
} | ||
// update helpers | ||
@@ -870,3 +991,3 @@ | ||
Object.assign(this[store], stateUpdate); | ||
if (store !== `state` || this.shouldUpdate(this[store])) { | ||
if (store !== `state` || this.shouldComponentUpdate(null, this[store])) { | ||
this.domPatcher.update(this.state); | ||
@@ -873,0 +994,0 @@ |
@@ -19,3 +19,4 @@ /** | ||
import delayedClassModule from 'snabbdom-delayed-class'; | ||
import {getNow} from './component-utils/perf'; | ||
import {Perf} from './component-utils/perf'; | ||
import {paramsModule} from './component-utils/snabbdom-params-module'; | ||
@@ -30,2 +31,3 @@ const patch = initSnabbdom([ | ||
delayedClassModule, | ||
paramsModule, | ||
]); | ||
@@ -98,3 +100,3 @@ | ||
const startedAt = getNow(); | ||
const startedAt = Perf.getNow(); | ||
this.rendering = true; | ||
@@ -110,3 +112,3 @@ this.pending = false; | ||
if (this.postRenderCallback) { | ||
this.postRenderCallback(getNow() - startedAt); | ||
this.postRenderCallback(Perf.getNow() - startedAt); | ||
} | ||
@@ -113,0 +115,0 @@ } |
@@ -10,2 +10,4 @@ // Type definitions for panel | ||
import {JsxVNode, JsxVNodeProps} from 'snabbdom-jsx-lite'; | ||
export class StateStore<State> { | ||
@@ -75,9 +77,14 @@ constructor(options: {store?: StateStore<State>}); | ||
export interface ConfigOptions<StateT, AppStateT = unknown, ContextRegistryT = unknown> { | ||
export interface ConfigOptions<StateT, AppStateT = unknown, ContextRegistryT = unknown, ParamT = unknown> { | ||
/** Function transforming state object to virtual dom tree */ | ||
template(scope?: StateT): VNode; | ||
params?: {[param in keyof ParamT]: InferType<ParamT[param]> | ParamType<ParamT[param]>}; | ||
/** Component-specific Shadow DOM stylesheet */ | ||
css?: string; | ||
/** object to provide default value for params */ | ||
defaultParams?: Partial<ParamT>; | ||
/** An initial default value for the component's state property */ | ||
@@ -153,2 +160,23 @@ defaultState?: StateT; | ||
type InferType<T> = T extends string | ||
? StringConstructor | ||
: T extends number | ||
? NumberConstructor | ||
: T extends boolean | ||
? BooleanConstructor | ||
: T extends unknown[] | ||
? ArrayConstructor | ||
: T extends Map<unknown, unknown> | ||
? MapConstructor | ||
: T extends Set<unknown> | ||
? SetConstructor | ||
: T extends (...args: any[]) => any | ||
? FunctionConstructor | ||
: ObjectConstructor; | ||
interface ParamType<T> { | ||
type: InferType<T>; | ||
required?: boolean; | ||
} | ||
export class Component< | ||
@@ -159,3 +187,4 @@ StateT, | ||
AppT = unknown, | ||
ContextRegistryT = unknown | ||
ContextRegistryT = unknown, | ||
ParamT extends Record<string, any> = unknown | ||
> extends WebComponent { | ||
@@ -171,2 +200,5 @@ /** The first Panel Component ancestor in the DOM tree; null if this component is the root */ | ||
/** New panel params */ | ||
params: Readonly<ParamT>; | ||
/** A reference to the top-level component */ | ||
@@ -206,3 +238,3 @@ app: AppT; | ||
/** Defines standard component configuration */ | ||
get config(): ConfigOptions<StateT, AppStateT, ContextRegistryT>; | ||
get config(): ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>; | ||
@@ -237,6 +269,6 @@ /** | ||
*/ | ||
getConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT>>(key: K): this['config'][K]; | ||
getConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>>(key: K): this['config'][K]; | ||
/** Sets a value in the component's configuration map after element initialization */ | ||
setConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT>>( | ||
setConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>>( | ||
key: K, | ||
@@ -247,2 +279,44 @@ val: ConfigOptions<StateT, AppStateT, ContextRegistryT>[K], | ||
/** | ||
* set the params for the this component | ||
* triggers a component update | ||
* if shouldComponentUpdate callback returns true | ||
*/ | ||
setParams(params: Partial<ParamT>): void; | ||
/** | ||
* Same API as react's `shouldComponentUpdate` usage | ||
* if child component implements this method, parent implmentation wil be discarded | ||
* only difference is the `params` or `state` could sometimes be null indicating that | ||
* the update is not related to `params` or `state` | ||
* | ||
* To be overridden by subclasses, defining conditional logic for whether | ||
* a component should rerender its template given the state and params to be applied. | ||
* In most cases this method can be left untouched, but can provide improved | ||
* performance when dealing with very many DOM elements. | ||
* | ||
* @example | ||
* shouldComponentUpdate(params, state) { | ||
* // don't need to rerender if result set ID hasn't changed | ||
* if (state && state.largeResultSetID === this._cachedResultID) { | ||
* return false | ||
* } | ||
* if (params && params.bookmark.id === this.params.bookmark.id) { | ||
* return false; | ||
* } | ||
* return !shallowEqual(params, this.params); | ||
* } | ||
*/ | ||
shouldComponentUpdate(params: ParamT | null, state: StateT | null): boolean; | ||
/** | ||
* To be overridden by subclasses, defining conditional logic for whether | ||
* a component should rerender its template given the state and params to be applied. | ||
* In most cases this method can be left untouched, but can provide improved | ||
* performance when dealing with very many DOM elements. | ||
* | ||
* @deprecated use shouldComponentUpdate instead | ||
*/ | ||
shouldUpdate(state: StateT): boolean; | ||
/** | ||
* Executes the route handler matching the given URL fragment, and updates | ||
@@ -261,10 +335,2 @@ * the URL, as though the user had navigated explicitly to that address. | ||
/** | ||
* To be overridden by subclasses, defining conditional logic for whether | ||
* a component should rerender its template given the state to be applied. | ||
* In most cases this method can be left untouched, but can provide improved | ||
* performance when dealing with very many DOM elements. | ||
*/ | ||
shouldUpdate(state: StateT): boolean; | ||
/** | ||
* Applies a state update specifically to app state shared across components. | ||
@@ -307,1 +373,29 @@ * In apps which don't specify `appState` in the root component config, all | ||
} | ||
/** | ||
* Panel component that only accepts 3 generic types | ||
*/ | ||
export class ParamComponent<ParamT = unknown, StateT = unknown, ContextRegistryT = unknown> extends Component< | ||
StateT, | ||
unknown, | ||
unknown, | ||
unknown, | ||
ContextRegistryT, | ||
ParamT | ||
> {} | ||
// define jsx IntrinsicElement inside namespace jsx to play well with react | ||
declare global { | ||
/** | ||
* opt-in jsx intrinsic global interfaces | ||
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking | ||
*/ | ||
namespace jsx { | ||
namespace JSX { | ||
type Element = JsxVNode; | ||
interface IntrinsicElements { | ||
[elemName: string]: JsxVNodeProps; | ||
} | ||
} | ||
} | ||
} |
@@ -13,2 +13,3 @@ /** | ||
import ComponentUtils from './component-utils'; | ||
import shallowEqual from './component-utils/shallowEqual'; | ||
import {h} from './dom-patcher'; | ||
@@ -19,3 +20,7 @@ import {jsx} from 'snabbdom-jsx-lite'; | ||
const ParamComponent = Component; | ||
export { | ||
/** {@link ParamComponent} newer class with modified generic types */ | ||
ParamComponent, | ||
/** {@link Component} class, to be subclassed by apps */ | ||
@@ -29,2 +34,4 @@ Component, | ||
StateStore, | ||
/** helper function for `shouldComponentUpdate` callback */ | ||
shallowEqual, | ||
/** | ||
@@ -31,0 +38,0 @@ * [snabbdom]{@link https://github.com/snabbdom/snabbdom} function to create Hyperscript nodes, |
{ | ||
"name": "panel", | ||
"version": "5.13.0", | ||
"version": "6.0.0", | ||
"description": "Web Components with Virtual DOM: lightweight composable web apps", | ||
@@ -23,6 +23,5 @@ "main": "build/index.js", | ||
"test": "npm run build-test && npm run test-server && npm run test-browser-local", | ||
"test-browser-local": "wct --plugin local test/browser/index.html", | ||
"test-browser-p": "wct --plugin local --persistent test/browser/index.html", | ||
"test-browser-sauce": "wct --plugin sauce test/browser/index.html", | ||
"test-server": "NODE_ENV=test mocha --require babel-core/register test/server", | ||
"test-browser-local": "SAUCE=0 karma start karma.config.js", | ||
"test-browser-sauce": "SAUCE=1 karma start karma.config.js", | ||
"test-server": "NODE_ENV=test nyc mocha --require @babel/register test/server", | ||
"tslint": "tslint -c tslint.json -t stylish 'lib/index.d.ts'", | ||
@@ -48,5 +47,6 @@ "type-check": "tsc" | ||
"dependencies": { | ||
"cuid": "1.3.8", | ||
"cuid": "2.1.6", | ||
"html-element": "2.3.0", | ||
"loader-utils": "1.1.0", | ||
"lodash-es": "4.17.21", | ||
"lodash.pick": "4.4.0", | ||
@@ -61,14 +61,12 @@ "raf": "3.2.0", | ||
"devDependencies": { | ||
"@babel/cli": "7.17.6", | ||
"@babel/core": "7.17.9", | ||
"@babel/preset-env": "7.16.11", | ||
"@babel/register": "7.17.7", | ||
"@webcomponents/custom-elements": "1.0.6", | ||
"@webcomponents/shadydom": "1.0.8", | ||
"babel-cli": "6.26.0", | ||
"babel-core": "6.26.3", | ||
"babel-loader": "6.2.4", | ||
"babel-plugin-syntax-async-functions": "6.13.0", | ||
"babel-plugin-transform-regenerator": "6.26.0", | ||
"babel-polyfill": "6.26.0", | ||
"babel-preset-es2015": "6.24.1", | ||
"babel-loader": "8.2.5", | ||
"chai": "4.2.0", | ||
"chrome-store-api": "1.0.5", | ||
"domsuite": "0.2.0", | ||
"domsuite": "0.6.0", | ||
"eslint": "6.8.0", | ||
@@ -78,7 +76,17 @@ "eslint-config-mixpanel": "4.0.0", | ||
"jsdoc": "3.6.5", | ||
"karma": "6.4.0", | ||
"karma-chrome-launcher": "3.1.1", | ||
"karma-firefox-launcher": "2.1.2", | ||
"karma-mocha": "2.0.1", | ||
"karma-sauce-launcher": "4.3.6", | ||
"karma-sourcemap-loader": "0.3.8", | ||
"karma-spec-reporter": "0.0.34", | ||
"lint-staged": "10.1.1", | ||
"minami": "1.1.1", | ||
"mocha": "2.5.3", | ||
"mocha": "9.2.2", | ||
"nyc": "15.1.0", | ||
"playwright": "1.24.2", | ||
"prettier": "2.0.2", | ||
"promisify-node": "0.4.0", | ||
"puppeteer": "16.1.0", | ||
"readline-sync": "1.4.7", | ||
@@ -89,5 +97,5 @@ "sinon": "9.2.3", | ||
"typescript": "4.0.2", | ||
"wct-browser-legacy": "1.0.2", | ||
"web-component-tester": "6.9.2", | ||
"webpack": "1.13.0", | ||
"util": "0.12.4", | ||
"webpack": "5.72.0", | ||
"webpack-cli": "4.9.2", | ||
"zip-folder": "1.0.0" | ||
@@ -94,0 +102,0 @@ }, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
33
3464
142897
11
38
+ Addedlodash-es@4.17.21
+ Addedcuid@2.1.6(transitive)
+ Addedlodash-es@4.17.21(transitive)
- Removedbrowser-fingerprint@0.0.1(transitive)
- Removedcore-js@1.2.7(transitive)
- Removedcuid@1.3.8(transitive)
- Removednode-fingerprint@0.0.2(transitive)
Updatedcuid@2.1.6