@qooxdoo/framework
Version:
The JS Framework for Coders
403 lines (366 loc) • 10.7 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2017 Zenesis Limited, http://www.zenesis.com
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* John Spackman (john.spackman@zenesis.com, @johnspackman)
************************************************************************ */
/**
* A mixin providing objects by ID and owners.
*
* The typical use of IDs is to override the `_createQxObjectImpl` method and create
* new instances on demand; all code should access these instances by calling
* `getQxObject`.
*/
qx.Mixin.define("qx.core.MObjectId", {
/*
* ****************************************************************************
* PROPERTIES
* ****************************************************************************
*/
properties: {
/** The owning object */
qxOwner: {
init: null,
check: "qx.core.Object",
nullable: true,
apply: "_applyQxOwner"
},
/** {String} The ID of the object. */
qxObjectId: {
init: null,
check(value) {
return (
value === null || (typeof value == "string" && value.indexOf("/") < 0)
);
},
nullable: true,
apply: "_applyQxObjectId"
}
},
/*
* ****************************************************************************
* MEMBERS
* ****************************************************************************
*/
statics: {
handleObjects(clazz, instance, id) {
const objectsDef = clazz.$$objects;
const clazzObject = objectsDef?.[id]?.call(instance);
if (clazzObject !== undefined) {
return clazzObject;
}
for (const mixin of clazz.$$includes ?? []) {
const mixinObject = qx.core.MObjectId.handleObjects(
mixin,
instance,
id
);
if (mixinObject !== undefined) {
return mixinObject;
}
}
return undefined;
}
},
members: {
__ownedQxObjects: null,
__changingQxOwner: false,
/**
* Apply owner
*/
_applyQxOwner(value, oldValue) {
if (!this.__changingQxOwner) {
throw new Error(
"Please use API methods to change owner, not the property"
);
}
},
/**
* Apply objectId
*/
_applyQxObjectId(value, oldValue) {
if (!this.__changingQxOwner) {
var owner = this.getQxOwner();
if (owner) {
owner.__onOwnedObjectIdChange(this, value, oldValue);
}
this._cascadeQxObjectIdChanges();
}
},
/**
* Called when a child's objectId changes
*/
__onOwnedObjectIdChange(obj, newId, oldId) {
delete this.__ownedQxObjects[oldId];
this.__ownedQxObjects[newId] = obj;
},
/**
* Reflect changes to IDs or owners
*/
_cascadeQxObjectIdChanges() {
if (typeof this.getContentElement == "function") {
var contentElement = this.getContentElement();
if (contentElement) {
contentElement.updateObjectId();
}
}
if (this.__ownedQxObjects) {
for (var name in this.__ownedQxObjects) {
var obj = this.__ownedQxObjects[name];
if (obj instanceof qx.core.Object) {
obj._cascadeQxObjectIdChanges();
}
}
}
},
/**
* Returns the object with the specified ID
*
* @param id
* {String} ID of the object
* @return {qx.core.Object?} the found object
*/
getQxObject(id) {
if (this.__ownedQxObjects) {
var obj = this.__ownedQxObjects[id];
if (obj !== undefined) {
return obj;
}
}
// Separate out the child control ID
var controlId = null;
var pos = id.indexOf("#");
if (pos > -1) {
controlId = id.substring(pos + 1);
id = id.substring(0, pos);
}
var result = undefined;
// Handle paths
if (id.indexOf("/") > -1) {
var segments = id.split("/");
var target = this;
var found = segments.every(segment => {
if (!segment.length) {
return true;
}
if (!target) {
return false;
}
var tmp;
if (segment === "..") {
tmp = target.getQxOwner();
} else {
tmp = target.getQxObject(segment);
}
if (tmp !== undefined) {
target = tmp;
return true;
}
});
if (found) {
result = target;
}
} else {
// No object, creating the object
result = this._createQxObject(id);
}
if (result && controlId) {
var childControl = result.getChildControl(controlId);
return childControl;
}
if (!qx.core.Environment.get("qx.core.Object.allowUndefinedObjectId")) {
if (result === undefined) {
throw new Error(
`Cannot find a QX Object in ${this.classname} [${this}] with id=${id}`
);
}
}
return result;
},
/**
* Creates the object and adds it to a list; most classes are expected to
* override `_createQxObjectImpl` NOT this method.
*
* @param id {String} ID of the object
* @return {qx.core.Object?} the created object
*/
_createQxObject(id) {
var result = this._createQxObjectImpl(id);
if (result !== undefined) {
this.addOwnedQxObject(result, id);
}
return result;
},
/**
* Creates the object, intended to be overridden. Null is a valid return
* value and will be cached by `getQxObject`, however `undefined` is NOT a
* valid value and so will not be cached meaning that `_createQxObjectImpl`
* will be called multiple times until a valid value is returned.
*
* @param id {String} ID of the object
* @return {qx.core.Object?} the created object
*/
_createQxObjectImpl(id) {
return undefined;
},
/**
* Adds an object as owned by this object
*
* @param obj {qx.core.Object} the object to register
* @param id {String?} the id to set when registering the object
*/
addOwnedQxObject(obj, id) {
if (!this.__ownedQxObjects) {
this.__ownedQxObjects = {};
}
if (!(obj instanceof qx.core.Object)) {
if (!id) {
throw new Error(
"Cannot register an object that has no ID, obj=" + obj
);
}
if (this.__ownedQxObjects[id]) {
throw new Error(
"Cannot register an object with ID '" +
id +
"' because that ID is already in use, this=" +
this +
", obj=" +
obj
);
}
this.__ownedQxObjects[id] = obj;
return;
}
var thatOwner = obj.getQxOwner();
if (thatOwner === this) {
return;
}
obj.__changingQxOwner = true;
try {
if (thatOwner) {
thatOwner.__removeOwnedQxObjectImpl(obj);
}
if (id === undefined) {
id = obj.getQxObjectId();
}
if (!id) {
throw new Error(
"Cannot register an object that has no ID, obj=" + obj
);
}
if (this.__ownedQxObjects[id]) {
throw new Error(
"Cannot register an object with ID '" +
id +
"' because that ID is already in use, this=" +
this +
", obj=" +
obj
);
}
if (obj.getQxOwner() != null) {
throw new Error(
"Cannot register an object with ID '" +
id +
"' because it is already owned by another object this=" +
this +
", obj=" +
obj
);
}
obj.setQxOwner(this);
obj.setQxObjectId(id);
obj._cascadeQxObjectIdChanges();
} finally {
obj.__changingQxOwner = false;
}
this.__ownedQxObjects[id] = obj;
},
/**
* Discards an object from the list of owned objects; note that this does
* not dispose of the object, simply forgets it if it exists.
*
* @param args {String|Object} the ID of the object to discard, or the object itself
*/
removeOwnedQxObject(args) {
if (!this.__ownedQxObjects) {
throw new Error(
"Cannot discard object because it is not owned by this, this=" +
this +
", object=" +
obj
);
}
var id;
var obj;
if (typeof args === "string") {
if (args.indexOf("/") > -1) {
throw new Error("Cannot discard owned objects based on a path");
}
id = args;
obj = this.__ownedQxObjects[id];
if (obj === undefined) {
return;
}
} else {
obj = args;
if (!(obj instanceof qx.core.Object)) {
throw new Error(
"Cannot discard object by reference because it is not a Qooxdoo object, please remove it using the original ID; object=" +
obj
);
}
id = obj.getQxObjectId();
if (this.__ownedQxObjects[id] !== obj) {
throw new Error(
"Cannot discard object because it is not owned by this, this=" +
this +
", object=" +
obj
);
}
}
if (obj !== null) {
if (!(obj instanceof qx.core.Object)) {
this.__removeOwnedQxObjectImpl(obj);
delete this.__ownedQxObjects[id];
} else {
obj.__changingQxOwner = true;
try {
this.__removeOwnedQxObjectImpl(obj);
obj._cascadeQxObjectIdChanges();
} finally {
obj.__changingQxOwner = false;
}
}
}
},
/**
* Removes an owned object
*
* @param obj {qx.core.Object} the object
*/
__removeOwnedQxObjectImpl(obj) {
if (obj !== null) {
var id = obj.getQxObjectId();
obj.setQxOwner(null);
delete this.__ownedQxObjects[id];
}
},
/**
* Returns an array of objects that are owned by this object, or an empty
* array if none exists.
*
* @return {Array}
*/
getOwnedQxObjects() {
return this.__ownedQxObjects ? Object.values(this.__ownedQxObjects) : [];
}
}
});