import { Flags, CancelablePromise } from '@uino/base-thing';
import { Utils } from '../common/Utils'
import { EventType } from '../const';
const __ = {
private: Symbol('private'),
}
const Flag = {
Changing: 1 << 0,
}
const cDestroyObjectEventTag = '__LevelManager_object_destroy_event';
/**
* @class LevelManager
* The level manager.
* @memberof THING
* @public
*/
class LevelManager {
/**
* The level manager to manage object(s) throught between parent and child object.
*/
constructor() {
this[__.private] = {};
let _private = this[__.private];
_private.enable = true;
_private.pendingPromises = [];
_private.changeOptions = null;
_private.prev = null;
_private.current = null;
_private.leavingObject = null;
_private.enteringObject = null;
_private.flags = new Flags();
_private.onFilterPath = null;
_private.app = Utils.getCurrentApp();
_private.levelControls = [];
_private.registerOrder = 0;
// Leave current level when it had been destroyed
_private.app.on(EventType.BeforeDestroy, '*', (ev) => {
if (_private.current == ev.object) {
this.quit();
}
}, cDestroyObjectEventTag);
}
// #region Private
// Notify event.
_notifyEvent(type, params) {
let _private = this[__.private];
let app = _private.app;
let object = params['current'];
// Skip for root enter level event
if (object == app.root) {
return;
}
// Trigger global events
app.trigger(type, params);
// Prevent root trigger duplicated event
if (object != app.root) {
object.trigger(type, params);
}
}
_notifyComplete(options, path) {
let _private = this[__.private];
// Clear change options
_private.changeOptions = null;
// Clear pendingPromises
_private.pendingPromises.length = 0;
// Finished change
_private.flags.enable(Flag.Changing, false);
// Build event to notify outside
let ev = {
prev: _private.prev,
current: _private.current,
path
};
let onComplete = Utils.parseValue(options['onComplete'], options['complete']);
if (onComplete) {
onComplete(ev);
}
_private.app.trigger(EventType.CompleteEnterLevel, ev);
_private.current.trigger(EventType.CompleteEnterLevel, ev);
}
_enterObjectLevel(object, options, path, origin) {
let _private = this[__.private];
_private.enteringObject = object;
let enterParam = {
path,
origin,
prev: _private.prev,
current: _private.current,
next: null,
options
};
enterParam.levelControls = this.getControlsByObject(object);
// Enter level
return object.level.enter(enterParam).then(() => {
// Finished level changed
object.level.finish(enterParam);
// Check whether reach the target object
if (object == path[path.length - 1]) {
this._notifyComplete(options, path);
}
_private.enteringObject = null;
});
}
_changeObject(object, options, path, origin) {
let _private = this[__.private];
return new CancelablePromise((resolve, reject, onCancel) => {
let leavePromise = null;
let enterPromise = null;
onCancel((cancelParam) => {
if (leavePromise) {
leavePromise.cancel();
}
if (enterPromise) {
enterPromise.cancel(cancelParam);
}
});
// Start to change
_private.flags.enable(Flag.Changing, true);
// Exit current level
if (_private.current && !_private.current.destroyed && _private.current != _private.leavingObject) {
_private.leavingObject = _private.current;
let leaveEventParam = this._generateEventParam(path, origin, object, options);
// Notify before leave level event
this._notifyEvent(EventType.BeforeLeaveLevel, leaveEventParam);
let leaveParam = Object.assign({}, leaveEventParam);
leaveParam.levelControls = this.getControlsByObject(_private.current);
// Leave level
leavePromise = _private.current.level.leave(leaveParam).then(() => {
// Notify leave level event
this._notifyEvent(EventType.LeaveLevel, leaveEventParam);
// Notify after leave level event
this._notifyEvent(EventType.AfterLeaveLevel, leaveEventParam);
_private.leavingObject = null;
});
}
else {
leavePromise = CancelablePromise.resolve();
}
leavePromise.then(() => {
// Update level
_private.prev = _private.current;
_private.current = object;
if (object.destroyed) {
resolve();
return;
}
let enterEventParam = this._generateEventParam(path, origin, null, options);
// Notify before enter level event
this._notifyEvent(EventType.BeforeEnterLevel, enterEventParam);
// Enter object level
enterPromise = this._enterObjectLevel(object, options, path, origin).then(() => {
resolve();
// Notify enter level event
this._notifyEvent(EventType.EnterLevel, enterEventParam);
// Notify after enter level event
this._notifyEvent(EventType.AfterEnterLevel, enterEventParam);
enterPromise = null;
});
leavePromise = null;
});
});
}
_generateEventParam(path, origin, next, options) {
let _private = this[__.private];
return {
path,
origin,
prev: _private.prev,
current: _private.current,
next,
options
};
}
_startToChange(path, options) {
let _private = this[__.private];
// Try to filter path
let onFilterPath = _private.onFilterPath;
if (onFilterPath) {
path = onFilterPath(path) || path;
}
let origin = _private.current;
path.forEachAsync(obj => {
let promise = this._changeObject(obj, options, path, origin);
_private.pendingPromises.push(promise);
return promise;
});
}
_change(object, options) {
let _private = this[__.private];
// Get the current level
let current = _private.current;
// Skip to change for the same object level
if (current == object) {
return;
}
if (current && !current.destroyed) {
// Get path from current/root level to object
let path = current.getPathTo(object) || app.root.getPathTo(object);
// Start to change level
this._startToChange(path, options);
}
else {
// Get the level path from parents
let path = object.parents.reverse();
path.push(object);
// Start to change level
this._startToChange(path, options);
}
}
_cancelPendingPromises(object, options) {
let _private = this[__.private];
if (!_private.pendingPromises.length) {
return;
}
if (!_private.current) {
return;
}
let path = _private.current.getPathTo(object);
let levelControls = this.getControlsByObject(_private.current);
let cancelParam = {
origin: _private.current,
prev: _private.prev,
current: _private.current,
next: object,
path: path,
options,
levelControls
}
_private.pendingPromises.forEach(promise => {
promise.cancel(cancelParam);
});
const changeOptions = _private.changeOptions;
if (changeOptions) {
let onStop = Utils.parseValue(changeOptions['onStop'], changeOptions['stop']);
if (onStop) {
onStop();
}
_private.changeOptions = null;
}
// Record options
_private.changeOptions = options;
_private.pendingPromises.length = 0;
}
_createCancelablePromise(object, options) {
return new CancelablePromise((resolve, reject, onCancel) => {
onCancel(() => { });
let copyOptions = Object.assign({}, options);
let onComplete = options['onComplete'] || options['complete'];
let onStop = options['onStop'] || options['stop'];
copyOptions.onComplete = copyOptions.complete = (ev) => {
if (onComplete) {
onComplete(ev);
}
resolve('onComplete');
};
copyOptions.onStop = copyOptions.stop = (ev) => {
if (onStop) {
onStop(ev);
}
resolve('onStop');
}
this._change(object, copyOptions);
});
}
_getControlByTag(condition, tag) {
let _private = this[__.private];
let levelControls = _private.levelControls;
for (let i = 0; i < levelControls.length; i++) {
const controlData = levelControls[i];
if (controlData.condition === condition) {
if (!tag) {
continue;
}
if (controlData.tag === tag) {
return controlData.control;
}
}
}
return null;
}
// #endregion
dispose() {
let _private = this[__.private];
let app = _private.app;
app.off(EventType.AfterDestroy, '*', cDestroyObjectEventTag);
this._cancelPendingPromises(null);
_private.flags.clear();
_private.prev = null;
_private.current = null;
_private.levelControls = null;
_private.registerOrder = null;
}
/**
* The function to call when level changed.
* @callback LevelChangedCallback
* @param {Object} ev The event info.
* @param {THING.BaseObject} ev.current The current level.
* @param {THING.BaseObject} ev.prev The previous level.
* @param {Array<THING.BaseObject>} ev.path The path from start to target object.
*/
/**
* @typedef {Object} LevelChangeOptions
* @param {LevelChangedCallback} onStop The callback function would be trigged when stop level change.
* @param {LevelChangedCallback} onComplete The callback function would be trigged when complete level change.
*/
/**
* Change current level.
* @param {THING.BaseObject} object The object.
* @param {LevelChangeOptions} options The options.
* @public
* @example
* let app = THING.App.current;
* let level = app.level;
* let target = app.query('.Entity')[0];
* level.change(target, {
* onComplete: function(){
* let ret = level.current == target;
* // @expect(ret == true);
* }
* });
*/
change(object, options = {}) {
return this.changeAsync(object, options);
}
/**
* Change current level in async mode.
* @param {THING.BaseObject} object The object.
* @param {LevelChangeOptions} options The options.
* @returns {Promise<any>}
* @example
* let app = THING.App.current;
* let level = app.level;
* let target = app.query('.Entity')[0];
* level.changeAsync(target, {
* onComplete: function(){
* let ret = level.current == target;
* // @expect(ret == true);
* }
* });
*/
changeAsync(object, options = {}) {
let _private = this[__.private];
// Skip for disable state
if (!_private.enable) {
return Promise.resolve();
}
// Skip to change for the same object level
let current = _private.current;
if (current == object) {
return Promise.resolve();
}
this._cancelPendingPromises(object, options);
// Start to change level
const promise = this._createCancelablePromise(object, options);
_private.pendingPromises.push(promise);
return promise;
}
/**
* Change to the parent level.
* @param {LevelChangeOptions} options The options.
* @public
* @example
* let app = THING.App.current;
* let level = app.level;
* let target = app.query('.Entity')[0];
* level.change(target, {
* onComplete: function(){
* let ret = level.current == target;
* // @expect(ret == true);
* level.back({
* onComplete: function(){
* ret = level.current == target.parent;
* // @expect(ret == true)
* }
* });
* }
* });
*/
back(options = {}) {
let _private = this[__.private];
let current = _private.current;
if (!current) {
return Promise.resolve();
}
let parent = current.parent;
if (!parent || parent.isRootObject) {
return Promise.resolve();
}
return this.changeAsync(parent, options);
}
/**
* Change to the parent level in async mode.
* @param {LevelChangeOptions} options The options.
* @returns {Promise<any>}
* @example
* let app = THING.App.current;
* let level = app.level;
* let target = app.query('.Entity')[0];
* level.change(target, {
* onComplete: function(){
* let ret = level.current == target;
* // @expect(ret == true);
* level.backAsync({
* onComplete: function(){
* ret = level.current == target.parent;
* // @expect(ret == true)
* }
* });
* }
* });
*/
backAsync(options = {}) {
let _private = this[__.private];
// Skip for disable state
if (!_private.enable) {
return;
}
let current = _private.current;
if (!current) {
return Promise.resolve();
}
let parent = current.parent;
if (!parent || parent.isRootObject) {
return Promise.resolve();
}
this._cancelPendingPromises(parent, options);
const promise = this._createCancelablePromise(parent, options);
_private.pendingPromises.push(promise);
return promise;
}
/**
* Quit.
* @returns {Promise<any>}
* @public
* @example
* let app = THING.App.current;
* let level = app.level;
* level.quit({
* onComplete: function(){
* let ret = level.current == null;
* // @expect(ret == true);
* }
* });
*/
quit() {
let _private = this[__.private];
_private.flags.clear();
// Exit current level
if (_private.current) {
this._cancelPendingPromises(_private.current, null);
let params = {
origin: null,
prev: _private.prev,
current: _private.current,
next: null,
};
this._notifyEvent(EventType.BeforeLeaveLevel, params);
let levelControls = this.getControlsByObject(_private.current);
return _private.current.level.leave({
path: [_private.current],
origin: null,
prev: null,
current: _private.current,
next: null,
levelControls
}).then(() => {
this._notifyEvent(EventType.AfterLeaveLevel, params);
_private.prev = null;
_private.current = null;
});
}
else {
return Promise.resolve();
}
}
/**
* @typedef {Object} RegisterControlOptions
* @param {Number} priority The level control priority. The default value is 0. A smaller value is executed first.
* @param {String} tag The level control tag.
*/
/**
* Register level control
* @param {String} condition Query conditions (The level control will apply to query results)
* @param {THING.BaseLevelControl} control The level control
* @param {RegisterControlOptions} options The level control options
* @public
*/
register(condition, control, options = {}) {
let _private = this[__.private];
if (!condition || !control) {
return;
}
// Check Repeate
for (let i = 0; i < _private.levelControls.length; i++) {
const controlData = _private.levelControls[i];
if (controlData.control == control) {
Utils.warn('Cannot be repeat registered!');
return;
}
}
// Parse options
const priority = options.priority || 0;
const tag = options.tag;
// Unregister the exist control
if (tag) {
let control = this._getControlByTag(condition, tag);
if (control) {
this.unregister(condition, tag);
}
}
// Produce control data
let order = ++_private.registerOrder;
let controlData = { control, condition, tag, priority, order };
// Push
_private.levelControls.push(controlData);
// Setup app
Object.defineProperty(control, "app", {
get: () => {
return _private.app;
},
configurable: true
});
// Sort array by priority and order
_private.levelControls.sort((a, b) => {
if (a.priority != b.priority) {
return b.priority - a.priority;
}
return a.order - b.order;
});
}
/**
* Unregister level control
* @param {String} condition Query conditions
* @param {String} tag The level control tag.
* @public
*/
unregister(condition, tag) {
if (!condition) {
return;
}
let _private = this[__.private];
let levelControls = _private.levelControls;
for (let i = levelControls.length - 1; i >= 0; i--) {
const controlData = levelControls[i];
if (controlData.condition === condition) {
if (!tag || controlData.tag === tag) {
let control = controlData.control;
if (control.isRunning) {
Utils.warn('Cannot be unregister while running!');
continue;
}
delete control.app;
levelControls.splice(i, 1);
}
}
}
}
unregisterByControl(control) {
if (!control) {
return;
}
let _private = this[__.private];
let levelControls = _private.levelControls;
for (let i = 0; i < levelControls.length; i++) {
const controlData = levelControls[i];
if (controlData.control == control) {
levelControls.splice(i, 1);
}
}
}
getControlsByObject(object) {
let _private = this[__.private];
const levelControls = _private.levelControls;
let result = [];
let testResultCache = {};
for (let i = 0; i < levelControls.length; i++) {
const controlData = levelControls[i];
const condition = controlData.condition;
// Get the test result by cache
let testResult = testResultCache[condition];
// If it is null, test it
if (Utils.isNull(testResult)) {
testResult = testResultCache[condition] = object.test(condition);
}
// If true, add to the result array
if (testResult === true) {
result.push(controlData.control);
}
}
return result;
}
/**
* Enable/Disable level manager.
* @type {Boolean}
* @example
* let app = THING.App.current;
* let level = app.level;
* level.enable = false;
* let target = app.query('.Entity')[0];
* level.change(target);
* let ret = level.isChanging;
* // @expect(ret == false);
*/
get enable() {
let _private = this[__.private];
return _private.enable;
}
set enable(value) {
let _private = this[__.private];
_private.enable = value;
}
/**
* The function to call when start to get object level path.
* @callback FilterLevelPathCallback
* @param {Array<THING.BaseObject>} path The level path.
* @returns {Array<THING.BaseObject>} The new level path, null or undefined indicates use the current level path.
*/
/**
* Get/Set filter path callback function.
* @type {FilterLevelPathCallback}
* @private
*/
get onFilterPath() {
let _private = this[__.private];
return _private.onFilterPath;
}
set onFilterPath(value) {
let _private = this[__.private];
_private.onFilterPath = value;
}
/**
* Get the previous object.
* @type {THING.BaseObject}
* @public
* @example
* let app = THING.App.current;
* let level = app.level;
* let prev = level.prev;
* let target = app.query('.Entity')[0];
* level.change(target, {
* onComplete: function(){
* let ret = level.prev == prev;
* // @expect(ret == true);
* }
* });
*/
get prev() {
let _private = this[__.private];
return _private.prev;
}
/**
* Get the current object.
* @type {THING.BaseObject}
* @public
* @example
* let app = THING.App.current;
* let level = app.level;
* let target = app.query('.Entity')[0];
* level.change(target, {
* onComplete: function(){
* let ret = level.current == target;
* // @expect(ret == true);
* }
* });
*/
get current() {
let _private = this[__.private];
return _private.current;
}
/**
* Check whether is changing level.
* @type {Boolean}
* @public
* @example
* let app = THING.App.current;
* let level = app.level;
* let target = app.query('.Entity')[0];
* level.change(target);
* let ret = level.isChanging;
* // @expect(ret == true);
*/
get isChanging() {
let _private = this[__.private];
return _private.flags.has(Flag.Changing);
}
}
export { LevelManager }