/**
* @module xblocks-core/block
*/
import * as xtag from 'xtag';
import forEach from 'lodash/forEach';
import isFunction from 'lodash/isFunction';
import stubFalse from 'lodash/stubFalse';
import merge from 'lodash/merge';
import assign from 'lodash/assign';
import mergeWith from 'lodash/mergeWith';
import uniqueId from 'lodash/uniqueId';
import spread from 'lodash/spread';
import castArray from 'lodash/castArray';
import isArray from 'lodash/isArray';
import get from 'lodash/get';
import wrap from 'lodash/wrap';
import invoke from 'lodash/invoke';
import trim from 'lodash/trim';
import * as dom from './dom';
import { XBElement } from './element';
import { lazy, propTypes } from './utils';
import Constants from './constants';
import wrapperFunction from './utils/wrapperFunction';
import checkOverriddenMethods from './utils/checkOverriddenMethods';
const spreadMergeWith = spread(mergeWith);
const BLOCK_COMMON_ACCESSORS = {
mounted: {
/**
* Check react mounted
* @returns {boolean}
* @readonly
*/
get: function () {
return Boolean(invoke(this, [ Constants.BLOCK, 'isMounted' ]));
}
},
content: {
/**
* Receiving the content.
* @returns {?string}
*/
get: function () {
const content = this.mounted ?
invoke(this, [ Constants.BLOCK, 'getMountedContent' ]) :
dom.contentNode(this).innerHTML;
return trim(content);
},
/**
* Installing a new content.
* @param {string} content
*/
set: function (content) {
content = trim(content);
if (this.mounted) {
invoke(this, [ Constants.BLOCK, 'setMountedContent' ], content);
} else {
dom.contentNode(this).innerHTML = content;
this.upgrade();
}
}
},
attrs: {
/**
* Getting object attributes.
* @returns {Object}
* @readonly
*/
get: function () {
return dom.attrs.toObject(this);
}
},
props: {
/**
* Getting object properties.
* @returns {Object}
* @readonly
*/
get: function () {
const props = dom.attrs.toObject(this);
const xprops = this.xprops;
const eprops = get(xtag, [ 'tags', this[ Constants.TAGNAME ], 'accessors' ], {});
for (let prop in eprops) {
if (xprops.hasOwnProperty(prop) &&
eprops.hasOwnProperty(prop) &&
!BLOCK_COMMON_ACCESSORS.hasOwnProperty(prop)) {
props[ prop ] = this[ prop ];
}
}
dom.attrs.typeConversion(props, xprops);
return props;
}
},
xprops: {
/**
* Getting react properties.
* @returns {Object}
* @readonly
*/
get: function () {
return propTypes(this[ Constants.TAGNAME ]);
}
},
outerHTML: dom.outerHTML
};
const BLOCK_COMMON_METHODS = {
/**
* Obtaining the React components.
* @returns {?Constructor}
*/
getComponent: function () {
return invoke(this, [ Constants.BLOCK, 'getUserComponent' ]);
},
/**
* Recalculation of the internal structure.
*/
upgrade: function () {
dom.upgradeAll(this);
},
/**
* Cloning a node.
* @param {boolean} deep true if the content to be saved
* @returns {HTMLElement}
*/
cloneNode: function (deep) {
// not to clone the contents
const node = dom.cloneNode(this, false);
dom.upgrade(node);
node[ Constants.TMPL ] = this[ Constants.TMPL ];
node[ Constants.INSERTED ] = false;
if (deep) {
node.content = this.content;
}
// ???
// if ('checked' in this) clone.checked = this.checked;
return node;
}
};
/**
* Creating a new tag.
* @see http://x-tag.github.io/
* @alias module:xblocks-core/block.create
* @param {string} blockName the name of the new node
* @param {?Object|array} options settings tag creation
* @returns {HTMLElement}
* @public
*/
export function create(blockName, options) {
options = castArray(options);
options.unshift({ lifecycle: { created: lifecycleCreated, inserted: lifecycleInserted } });
options.push(mergeCustomizer);
options = spreadMergeWith(options);
forEach(options.accessors, accessorsIterator);
options.accessors = assign(options.accessors, BLOCK_COMMON_ACCESSORS);
options.methods = assign(options.methods, BLOCK_COMMON_METHODS);
// "removed" should be called after user handler
options.lifecycle.removed = wrap(options.lifecycle.removed, wrap(lifecycleRemoved, wrapperFunction));
return xtag.register(blockName, options);
}
/**
* Initialization of the element.
* @example
* blockInit(node);
* @param {HTMLElement} node
* @returns {boolean}
* @private
*/
function blockInit(node) {
if (!node[ Constants.TAGNAME ]) {
node[ Constants.INSERTED ] = false;
node[ Constants.TAGNAME ] = node.tagName.toLowerCase();
node[ Constants.TMPL ] = {};
node[ Constants.UID ] = uniqueId();
return true;
}
return false;
}
/**
* Creating an item.
* @example
* blockCreate(node);
* @param {HTMLElement} node
* @private
*/
function blockCreate(node) {
if (node.hasChildNodes()) {
Array.prototype.forEach.call(
node.querySelectorAll('script[type="text/x-template"][ref],template[ref]'),
tmplCompileIterator,
node
);
}
node[ Constants.BLOCK ] = new XBElement(node);
}
/**
* Pending the creation of the item.
* @example
* blockCreateLazy([ node1, node2, ... ]);
* @param {HTMLElement[]} nodes
* @private
*/
function blockCreateLazy(nodes) {
nodes.forEach(blockCreate);
}
/**
* The selection of templates.
* @example
* // append template to node
* tmplCompileIterator.call(node, tmplNode);
* @param {HTMLElement} node
* @this HTMLElement
* @private
*/
function tmplCompileIterator(node) {
this[ Constants.TMPL ][ node.getAttribute('ref') ] = node.innerHTML;
}
/**
* Special handler of merge.
* Arrays are merged by the concatenation.
* @example
* _.mergeWith(obj, src, mergeCustomizer);
* @param {*} objValue
* @param {*} srcValue
* @param {string} key
* @returns {Object|array|undefined}
* @throws The following methods are overridden
* @private
*/
function mergeCustomizer(objValue, srcValue, key) {
if (isArray(objValue)) {
return objValue.concat(srcValue);
}
if (key === 'lifecycle') {
return mergeWith(objValue, srcValue, lifecycleCustomizer);
}
if (key === 'events') {
return mergeWith(objValue, srcValue, eventsCustomizer);
}
if (key === 'accessors') {
return mergeWith(objValue, srcValue, accessorsCustomizer);
}
if (key === 'methods') {
checkOverriddenMethods(objValue, srcValue);
}
}
/**
* Inheritance lifecycle handler.
* @example
* _.mergeWith(objValue, srcValue, lifecycleCustomizer);
* @param {function} [objValue] the current handler
* @param {function} [srcValue] the new handler
* @returns {function}
* @private
*/
function lifecycleCustomizer(objValue, srcValue) {
return wrap(objValue, wrap(srcValue, wrapperFunction));
}
/**
* Inheritance event handler.
* @example
* _.mergeWith(objValue, srcValue, eventsCustomizer);
* @param {function} [objValue] the current handler
* @param {function} [srcValue] the new handler
* @returns {function}
* @private
*/
function eventsCustomizer(objValue, srcValue) {
return wrap(objValue, wrap(srcValue, wrapperEvents));
}
/**
* Inheritance events "set" property changes.
* @example
* _.mergeWith(objValue, srcValue, accessorsCustomizer);
* @param {Object} [objValue] the current value
* @param {Object} [srcValue] the new value
* @returns {Object}
* @private
*/
function accessorsCustomizer(objValue, srcValue) {
const objSetter = get(objValue, 'set');
const srcSetter = get(srcValue, 'set');
return merge({}, objValue, srcValue, {
set: wrap(objSetter, wrap(srcSetter, wrapperFunction))
});
}
/**
* Implementation of inherited event.
* @example
* // call objFunc, srcFunc
* _.wrap(objFunc, _.wrap(srcFunc, wrapperEvents));
* @param {function} [srcFunc]
* @param {function} [objFunc]
* @param {...*} args
* @private
*/
function wrapperEvents(srcFunc, objFunc, ...args) {
const event = (args[ 0 ] instanceof Event) && args[ 0 ];
const isStopped = event ? () => event.immediatePropagationStopped : stubFalse;
if (!isStopped() && isFunction(objFunc)) {
objFunc.apply(this, args);
}
if (!isStopped() && isFunction(srcFunc)) {
srcFunc.apply(this, args);
}
}
/**
* The assignment of parameters accessors.
* @example
* _.forEach({}, accessorsIterator);
* @param {Object} options
* @param {string} name
* @param {Object} accessors
* @private
*/
function accessorsIterator(options, name, accessors) {
const optionsSetter = get(options, 'set');
const updateSetter = wrap(name, wrapperAccessorsSetUpdate);
accessors[ name ] = merge({}, options, {
set: wrap(optionsSetter, wrap(updateSetter, wrapperFunction))
});
}
/**
* Update element when a property is changed.
* @example
* // call node.xblock.update();
* _.wrap("accessor-name", wrapperAccessorsSetUpdate).call(node, 'newValue', 'oldValue');
* @param {string} accessorName the name of the property
* @param {*} nextValue
* @param {*} prevValue
* @this HTMLElement
* @private
*/
function wrapperAccessorsSetUpdate(accessorName, nextValue, prevValue) {
if (nextValue !== prevValue && this.xprops.hasOwnProperty(accessorName) && this.mounted) {
this[ Constants.BLOCK ].update();
}
}
/**
* The callback of the remote in DOM.
* @this HTMLElement
* @private
*/
function lifecycleRemoved() {
this[ Constants.INSERTED ] = false;
const block = this[ Constants.BLOCK ];
if (block) {
block.destroy();
this[ Constants.BLOCK ] = undefined;
}
}
/**
* The callback of the create element.
* @this HTMLElement
* @private
*/
function lifecycleCreated() {
blockInit(this);
}
/**
* The callback of the insert in DOM.
* @this HTMLElement
* @private
*/
function lifecycleInserted() {
if (this[ Constants.INSERTED ]) {
return;
}
blockInit(this);
this[ Constants.INSERTED ] = true;
const isScriptContent = Boolean(this.querySelector('script'));
// asynchronous read content
// <xb-test><script>...</script><div>not found</div></xb-test>
if (isScriptContent) {
lazy(blockCreateLazy, this);
} else {
blockCreate(this);
}
}