ember-source
Version:
A JavaScript framework for creating ambitious web applications
602 lines (561 loc) • 18.3 kB
JavaScript
import { I as INIT_FACTORY } from '../../shared-chunks/registry-B8WARvkP.js';
import { meta, peekMeta } from '../-internals/meta/lib/meta.js';
import { R as ROOT, g as guidFor, o as observerListenerMetaFor, w as wrap } from '../../shared-chunks/mandatory-setter-BiXq-dpN.js';
import { isDevelopingApp } from '@embroider/macros';
import '../debug/index.js';
import { y as defineValue, z as defineDecorator, A as revalidateObservers, B as nativeDescDecorator, f as isClassicDecorator, E as descriptorForDecorator, C as ComputedProperty, F as makeComputedDecorator, v as addObserver, w as removeObserver, G as addListener, H as removeListener } from '../../shared-chunks/cache-DORQczuy.js';
import '../../@glimmer/validator/index.js';
import '../../@glimmer/destroyable/index.js';
import '../../@glimmer/manager/index.js';
import { s as setUnprocessedMixins } from '../../shared-chunks/namespace_search-DU-a-C1F.js';
import { assert } from '../debug/lib/assert.js';
/**
@module @ember/object/mixin
*/
const a_concat = Array.prototype.concat;
const {
isArray
} = Array;
function extractAccessors(properties) {
if (properties !== undefined) {
for (let key of Object.keys(properties)) {
let desc = Object.getOwnPropertyDescriptor(properties, key);
if (desc.get !== undefined || desc.set !== undefined) {
Object.defineProperty(properties, key, {
value: nativeDescDecorator(desc)
});
}
}
}
return properties;
}
function concatenatedMixinProperties(concatProp, props, values, base) {
// reset before adding each new mixin to pickup concats from previous
let concats = values[concatProp] || base[concatProp];
if (props[concatProp]) {
concats = concats ? a_concat.call(concats, props[concatProp]) : props[concatProp];
}
return concats;
}
function giveDecoratorSuper(key, decorator, property, descs) {
if (property === true) {
return decorator;
}
let originalGetter = property._getter;
if (originalGetter === undefined) {
return decorator;
}
let superDesc = descs[key];
// Check to see if the super property is a decorator first, if so load its descriptor
let superProperty = typeof superDesc === 'function' ? descriptorForDecorator(superDesc) : superDesc;
if (superProperty === undefined || superProperty === true) {
return decorator;
}
let superGetter = superProperty._getter;
if (superGetter === undefined) {
return decorator;
}
let get = wrap(originalGetter, superGetter);
let set;
let originalSetter = property._setter;
let superSetter = superProperty._setter;
if (superSetter !== undefined) {
if (originalSetter !== undefined) {
set = wrap(originalSetter, superSetter);
} else {
// If the super property has a setter, we default to using it no matter what.
// This is clearly very broken and weird, but it's what was here so we have
// to keep it until the next major at least.
//
// TODO: Add a deprecation here.
set = superSetter;
}
} else {
set = originalSetter;
}
// only create a new CP if we must
if (get !== originalGetter || set !== originalSetter) {
// Since multiple mixins may inherit from the same parent, we need
// to clone the computed property so that other mixins do not receive
// the wrapped version.
let dependentKeys = property._dependentKeys || [];
let newProperty = new ComputedProperty([...dependentKeys, {
get,
set
}]);
newProperty._readOnly = property._readOnly;
newProperty._meta = property._meta;
newProperty.enumerable = property.enumerable;
// SAFETY: We passed in the impl for this class
return makeComputedDecorator(newProperty, ComputedProperty);
}
return decorator;
}
function giveMethodSuper(key, method, values, descs) {
// Methods overwrite computed properties, and do not call super to them.
if (descs[key] !== undefined) {
return method;
}
// Find the original method in a parent mixin
let superMethod = values[key];
// Only wrap the new method if the original method was a function
if (typeof superMethod === 'function') {
return wrap(method, superMethod);
}
return method;
}
function simpleMakeArray(value) {
if (!value) {
return [];
} else if (!Array.isArray(value)) {
return [value];
} else {
return value;
}
}
function applyConcatenatedProperties(key, value, values) {
let baseValue = values[key];
let ret = simpleMakeArray(baseValue).concat(simpleMakeArray(value));
if (isDevelopingApp()) {
// it is possible to use concatenatedProperties with strings (which cannot be frozen)
// only freeze objects...
if (typeof ret === 'object' && ret !== null) {
// prevent mutating `concatenatedProperties` array after it is applied
Object.freeze(ret);
}
}
return ret;
}
function applyMergedProperties(key, value, values) {
let baseValue = values[key];
(isDevelopingApp() && !(!isArray(value)) && assert(`You passed in \`${JSON.stringify(value)}\` as the value for \`${key}\` but \`${key}\` cannot be an Array`, !isArray(value)));
if (!baseValue) {
return value;
}
let newBase = Object.assign({}, baseValue);
let hasFunction = false;
let props = Object.keys(value);
for (let prop of props) {
let propValue = value[prop];
if (typeof propValue === 'function') {
hasFunction = true;
newBase[prop] = giveMethodSuper(prop, propValue, baseValue, {});
} else {
newBase[prop] = propValue;
}
}
if (hasFunction) {
newBase._super = ROOT;
}
return newBase;
}
function mergeMixins(mixins, meta, descs, values, base, keys, keysWithSuper) {
let currentMixin;
for (let i = 0; i < mixins.length; i++) {
currentMixin = mixins[i];
(isDevelopingApp() && !(typeof currentMixin === 'object' && currentMixin !== null && Object.prototype.toString.call(currentMixin) !== '[object Array]') && assert(`Expected hash or Mixin instance, got ${Object.prototype.toString.call(currentMixin)}`, typeof currentMixin === 'object' && currentMixin !== null && Object.prototype.toString.call(currentMixin) !== '[object Array]'));
if (MIXINS.has(currentMixin)) {
if (meta.hasMixin(currentMixin)) {
continue;
}
meta.addMixin(currentMixin);
let {
properties,
mixins
} = currentMixin;
if (properties !== undefined) {
mergeProps(meta, properties, descs, values, base, keys, keysWithSuper);
} else if (mixins !== undefined) {
mergeMixins(mixins, meta, descs, values, base, keys, keysWithSuper);
if (currentMixin instanceof Mixin && currentMixin._without !== undefined) {
currentMixin._without.forEach(keyName => {
// deleting the key means we won't process the value
let index = keys.indexOf(keyName);
if (index !== -1) {
keys.splice(index, 1);
}
});
}
}
} else {
mergeProps(meta, currentMixin, descs, values, base, keys, keysWithSuper);
}
}
}
function mergeProps(meta, props, descs, values, base, keys, keysWithSuper) {
let concats = concatenatedMixinProperties('concatenatedProperties', props, values, base);
let mergings = concatenatedMixinProperties('mergedProperties', props, values, base);
let propKeys = Object.keys(props);
for (let key of propKeys) {
let value = props[key];
if (value === undefined) continue;
if (keys.indexOf(key) === -1) {
keys.push(key);
let desc = meta.peekDescriptors(key);
if (desc === undefined) {
// If the value is a classic decorator, we don't want to actually
// access it, because that will execute the decorator while we're
// building the class.
if (!isClassicDecorator(value)) {
// The superclass did not have a CP, which means it may have
// observers or listeners on that property.
let prev = values[key] = base[key];
if (typeof prev === 'function') {
updateObserversAndListeners(base, key, prev, false);
}
}
} else {
descs[key] = desc;
// The super desc will be overwritten on descs, so save off the fact that
// there was a super so we know to Object.defineProperty when writing
// the value
keysWithSuper.push(key);
desc.teardown(base, key, meta);
}
}
let isFunction = typeof value === 'function';
if (isFunction) {
let desc = descriptorForDecorator(value);
if (desc !== undefined) {
// Wrap descriptor function to implement _super() if needed
descs[key] = giveDecoratorSuper(key, value, desc, descs);
values[key] = undefined;
continue;
}
}
if (concats && concats.indexOf(key) >= 0 || key === 'concatenatedProperties' || key === 'mergedProperties') {
value = applyConcatenatedProperties(key, value, values);
} else if (mergings && mergings.indexOf(key) > -1) {
value = applyMergedProperties(key, value, values);
} else if (isFunction) {
value = giveMethodSuper(key, value, values, descs);
}
values[key] = value;
descs[key] = undefined;
}
}
function updateObserversAndListeners(obj, key, fn, add) {
let meta = observerListenerMetaFor(fn);
if (meta === undefined) return;
let {
observers,
listeners
} = meta;
if (observers !== undefined) {
let updateObserver = add ? addObserver : removeObserver;
for (let path of observers.paths) {
updateObserver(obj, path, null, key, observers.sync);
}
}
if (listeners !== undefined) {
let updateListener = add ? addListener : removeListener;
for (let listener of listeners) {
updateListener(obj, listener, null, key);
}
}
}
function applyMixin(obj, mixins, _hideKeys = false) {
let descs = Object.create(null);
let values = Object.create(null);
let meta$1 = meta(obj);
let keys = [];
let keysWithSuper = [];
obj._super = ROOT;
// Go through all mixins and hashes passed in, and:
//
// * Handle concatenated properties
// * Handle merged properties
// * Set up _super wrapping if necessary
// * Set up computed property descriptors
// * Copying `toString` in broken browsers
mergeMixins(mixins, meta$1, descs, values, obj, keys, keysWithSuper);
for (let key of keys) {
let value = values[key];
let desc = descs[key];
if (value !== undefined) {
if (typeof value === 'function') {
updateObserversAndListeners(obj, key, value, true);
}
defineValue(obj, key, value, keysWithSuper.indexOf(key) !== -1, !_hideKeys);
} else if (desc !== undefined) {
defineDecorator(obj, key, desc, meta$1);
}
}
if (!meta$1.isPrototypeMeta(obj)) {
revalidateObservers(obj);
}
return obj;
}
/**
@method mixin
@param obj
@param mixins*
@return obj
@private
*/
function mixin(obj, ...args) {
applyMixin(obj, args);
return obj;
}
const MIXINS = new WeakSet();
/**
The `Mixin` class allows you to create mixins, whose properties can be
added to other classes. For instance,
```javascript
import Mixin from '@ember/object/mixin';
const EditableMixin = Mixin.create({
edit() {
console.log('starting to edit');
this.set('isEditing', true);
},
isEditing: false
});
```
```javascript
import EmberObject from '@ember/object';
import EditableMixin from '../mixins/editable';
// Mix mixins into classes by passing them as the first arguments to
// `.extend.`
const Comment = EmberObject.extend(EditableMixin, {
post: null
});
let comment = Comment.create({
post: somePost
});
comment.edit(); // outputs 'starting to edit'
```
Note that Mixins are created with `Mixin.create`, not
`Mixin.extend`.
Note that mixins extend a constructor's prototype so arrays and object literals
defined as properties will be shared amongst objects that implement the mixin.
If you want to define a property in a mixin that is not shared, you can define
it either as a computed property or have it be created on initialization of the object.
```javascript
// filters array will be shared amongst any object implementing mixin
import Mixin from '@ember/object/mixin';
import { A } from '@ember/array';
const FilterableMixin = Mixin.create({
filters: A()
});
```
```javascript
import Mixin from '@ember/object/mixin';
import { A } from '@ember/array';
import { computed } from '@ember/object';
// filters will be a separate array for every object implementing the mixin
const FilterableMixin = Mixin.create({
filters: computed(function() {
return A();
})
});
```
```javascript
import Mixin from '@ember/object/mixin';
import { A } from '@ember/array';
// filters will be created as a separate array during the object's initialization
const Filterable = Mixin.create({
filters: null,
init() {
this._super(...arguments);
this.set("filters", A());
}
});
```
@class Mixin
@public
*/
class Mixin {
/** @internal */
/** @internal */
mixins;
/** @internal */
properties;
/** @internal */
ownerConstructor;
/** @internal */
_without;
/** @internal */
constructor(mixins, properties) {
MIXINS.add(this);
this.properties = extractAccessors(properties);
this.mixins = buildMixinsArray(mixins);
this.ownerConstructor = undefined;
this._without = undefined;
if (isDevelopingApp()) {
// Eagerly add INIT_FACTORY to avoid issues in DEBUG as a result of Object.seal(mixin)
this[INIT_FACTORY] = null;
/*
In debug builds, we seal mixins to help avoid performance pitfalls.
In IE11 there is a quirk that prevents sealed objects from being added
to a WeakMap. Unfortunately, the mixin system currently relies on
weak maps in `guidFor`, so we need to prime the guid cache weak map.
*/
guidFor(this);
if (Mixin._disableDebugSeal !== true) {
Object.seal(this);
}
}
}
/**
@method create
@for @ember/object/mixin
@static
@param arguments*
@public
*/
static create(...args) {
setUnprocessedMixins();
let M = this;
return new M(args, undefined);
}
// returns the mixins currently applied to the specified object
// TODO: Make `mixin`
/** @internal */
static mixins(obj) {
let meta = peekMeta(obj);
let ret = [];
if (meta === null) {
return ret;
}
meta.forEachMixins(currentMixin => {
// skip primitive mixins since these are always anonymous
if (!currentMixin.properties) {
ret.push(currentMixin);
}
});
return ret;
}
/**
@method reopen
@param arguments*
@private
@internal
*/
reopen(...args) {
if (args.length === 0) {
return this;
}
if (this.properties) {
let currentMixin = new Mixin(undefined, this.properties);
this.properties = undefined;
this.mixins = [currentMixin];
} else if (!this.mixins) {
this.mixins = [];
}
this.mixins = this.mixins.concat(buildMixinsArray(args));
return this;
}
/**
@method apply
@param obj
@return applied object
@private
@internal
*/
apply(obj, _hideKeys = false) {
// Ember.NativeArray is a normal Ember.Mixin that we mix into `Array.prototype` when prototype extensions are enabled
// mutating a native object prototype like this should _not_ result in enumerable properties being added (or we have significant
// issues with things like deep equality checks from test frameworks, or things like jQuery.extend(true, [], [])).
//
// _hideKeys disables enumerablity when applying the mixin. This is a hack, and we should stop mutating the array prototype by default 😫
return applyMixin(obj, [this], _hideKeys);
}
/** @internal */
applyPartial(obj) {
return applyMixin(obj, [this]);
}
/**
@method detect
@param obj
@return {Boolean}
@private
@internal
*/
detect(obj) {
if (typeof obj !== 'object' || obj === null) {
return false;
}
if (MIXINS.has(obj)) {
return _detect(obj, this);
}
let meta = peekMeta(obj);
if (meta === null) {
return false;
}
return meta.hasMixin(this);
}
/** @internal */
without(...args) {
let ret = new Mixin([this]);
ret._without = args;
return ret;
}
/** @internal */
keys() {
let keys = _keys(this);
(isDevelopingApp() && !(keys) && assert('[BUG] Missing keys for mixin!', keys));
return keys;
}
/** @internal */
toString() {
return '(unknown mixin)';
}
}
if (isDevelopingApp()) {
Object.defineProperty(Mixin, '_disableDebugSeal', {
configurable: true,
enumerable: false,
writable: true,
value: false
});
}
function buildMixinsArray(mixins) {
let length = mixins && mixins.length || 0;
let m = undefined;
if (length > 0) {
m = new Array(length);
for (let i = 0; i < length; i++) {
let x = mixins[i];
(isDevelopingApp() && !(typeof x === 'object' && x !== null && Object.prototype.toString.call(x) !== '[object Array]') && assert(`Expected hash or Mixin instance, got ${Object.prototype.toString.call(x)}`, typeof x === 'object' && x !== null && Object.prototype.toString.call(x) !== '[object Array]'));
if (MIXINS.has(x)) {
m[i] = x;
} else {
m[i] = new Mixin(undefined, x);
}
}
}
return m;
}
if (isDevelopingApp()) {
Object.seal(Mixin.prototype);
}
function _detect(curMixin, targetMixin, seen = new Set()) {
if (seen.has(curMixin)) {
return false;
}
seen.add(curMixin);
if (curMixin === targetMixin) {
return true;
}
let mixins = curMixin.mixins;
if (mixins) {
return mixins.some(mixin => _detect(mixin, targetMixin, seen));
}
return false;
}
function _keys(mixin, ret = new Set(), seen = new Set()) {
if (seen.has(mixin)) {
return;
}
seen.add(mixin);
if (mixin.properties) {
let props = Object.keys(mixin.properties);
for (let prop of props) {
ret.add(prop);
}
} else if (mixin.mixins) {
mixin.mixins.forEach(x => _keys(x, ret, seen));
}
return ret;
}
export { applyMixin, Mixin as default, mixin };