283 lines
8.8 KiB
TypeScript

import Vue, { VNode } from 'vue';
import Handsontable from 'handsontable/base';
import { HotTableProps, VueProps, EditorComponent } from './types';
const unassignedPropSymbol = Symbol('unassigned');
let bulkComponentContainer = null;
/**
* Message for the warning thrown if the Handsontable instance has been destroyed.
*/
export const HOT_DESTROYED_WARNING = 'The Handsontable instance bound to this component was destroyed and cannot be' +
' used properly.';
/**
* Rewrite the settings object passed to the watchers to be a clean array/object prepared to use within Handsontable config.
*
* @param {*} observerSettings Watcher object containing the changed data.
* @returns {Object|Array}
*/
export function rewriteSettings(observerSettings): any[] | object {
const settingsType = Object.prototype.toString.call(observerSettings);
let settings: any[] | object | null = null;
let type: { array?: boolean, object?: boolean } = {};
if (settingsType === '[object Array]') {
settings = [];
type.array = true;
} else if (settingsType === '[object Object]') {
settings = {};
type.object = true;
}
if (type.array || type.object) {
for (const p in observerSettings) {
if (observerSettings.hasOwnProperty(p)) {
settings[p] = observerSettings[p];
}
}
} else {
settings = observerSettings;
}
return settings;
}
/**
* Private method to ensure the table is not calling `updateSettings` after editing cells.
* @private
*/
export function preventInternalEditWatch(component) {
if (component.hotInstance) {
component.hotInstance.addHook('beforeChange', () => {
component.__internalEdit = true;
});
component.hotInstance.addHook('beforeCreateRow', () => {
component.__internalEdit = true;
});
component.hotInstance.addHook('beforeCreateCol', () => {
component.__internalEdit = true;
});
component.hotInstance.addHook('beforeRemoveRow', () => {
component.__internalEdit = true;
});
component.hotInstance.addHook('beforeRemoveCol', () => {
component.__internalEdit = true;
});
}
}
/**
* Generate an object containing all the available Handsontable properties and plugin hooks.
*
* @param {String} source Source for the factory (either 'HotTable' or 'HotColumn').
* @returns {Object}
*/
export function propFactory(source): VueProps<HotTableProps> {
const registeredHooks: string[] = Handsontable.hooks.getRegistered();
let propSchema: VueProps<HotTableProps> = {};
Object.assign(propSchema, Handsontable.DefaultSettings);
for (let prop in propSchema) {
propSchema[prop] = {
default: unassignedPropSymbol
};
}
for (let i = 0; i < registeredHooks.length; i++) {
propSchema[registeredHooks[i]] = {
default: unassignedPropSymbol
};
}
propSchema.settings = {
default: unassignedPropSymbol
};
if (source === 'HotTable') {
propSchema.id = {
type: String,
default: 'hot-' + Math.random().toString(36).substring(5)
};
propSchema.wrapperRendererCacheSize = {
type: Number,
default: 3000
};
}
return propSchema;
}
/**
* Filter out all of the unassigned props, and return only the one passed to the component.
*
* @param {Object} props Object containing all the possible props.
* @returns {Object} Object containing only used props.
*/
export function filterPassedProps(props) {
const filteredProps: VueProps<HotTableProps> = {};
const columnSettingsProp = props['settings'];
if (columnSettingsProp !== unassignedPropSymbol) {
for (let propName in columnSettingsProp) {
if (columnSettingsProp.hasOwnProperty(propName) && columnSettingsProp[propName] !== unassignedPropSymbol) {
filteredProps[propName] = columnSettingsProp[propName];
}
}
}
for (let propName in props) {
if (props.hasOwnProperty(propName) && propName !== 'settings' && props[propName] !== unassignedPropSymbol) {
filteredProps[propName] = props[propName];
}
}
return filteredProps;
}
/**
* Prepare the settings object to be used as the settings for Handsontable, based on the props provided to the component.
*
* @param {HotTableProps} props The props passed to the component.
* @param {Handsontable.GridSettings} currentSettings The current Handsontable settings.
* @returns {Handsontable.GridSettings} An object containing the properties, ready to be used within Handsontable.
*/
export function prepareSettings(props: HotTableProps, currentSettings?: Handsontable.GridSettings): Handsontable.GridSettings {
const assignedProps: VueProps<HotTableProps> = filterPassedProps(props);
const hotSettingsInProps: {} = props.settings ? props.settings : assignedProps;
const additionalHotSettingsInProps: Handsontable.GridSettings = props.settings ? assignedProps : null;
const newSettings = {};
for (const key in hotSettingsInProps) {
if (
hotSettingsInProps.hasOwnProperty(key) &&
hotSettingsInProps[key] !== void 0 &&
((currentSettings && key !== 'data') ? !simpleEqual(currentSettings[key], hotSettingsInProps[key]) : true)
) {
newSettings[key] = hotSettingsInProps[key];
}
}
for (const key in additionalHotSettingsInProps) {
if (
additionalHotSettingsInProps.hasOwnProperty(key) &&
key !== 'id' &&
key !== 'settings' &&
key !== 'wrapperRendererCacheSize' &&
additionalHotSettingsInProps[key] !== void 0 &&
((currentSettings && key !== 'data') ? !simpleEqual(currentSettings[key], additionalHotSettingsInProps[key]) : true)
) {
newSettings[key] = additionalHotSettingsInProps[key];
}
}
return newSettings;
}
/**
* Get the VNode element with the provided type attribute from the component slots.
*
* @param {Array} componentSlots Array of slots from a component.
* @param {String} type Type of the child component. Either `hot-renderer` or `hot-editor`.
* @returns {Object|null} The VNode of the child component (or `null` when nothing's found).
*/
export function findVNodeByType(componentSlots: VNode[], type: string): VNode {
let componentVNode: VNode = null;
componentSlots.every((slot, index) => {
if (slot.data && slot.data.attrs && slot.data.attrs[type] !== void 0) {
componentVNode = slot;
return false;
}
return true;
});
return componentVNode;
}
/**
* Get all `hot-column` component instances from the provided children array.
*
* @param {Array} children Array of children from a component.
* @returns {Array} Array of `hot-column` instances.
*/
export function getHotColumnComponents(children) {
return children.filter((child) => child.$options.name === 'HotColumn');
}
/**
* Create an instance of the Vue Component based on the provided VNode.
*
* @param {Object} vNode VNode element to be turned into a component instance.
* @param {Object} parent Instance of the component to be marked as a parent of the newly created instance.
* @param {Object} props Props to be passed to the new instance.
* @param {Object} data Data to be passed to the new instance.
*/
export function createVueComponent(vNode: VNode, parent: Vue, props: object, data: object): EditorComponent {
const ownerDocument = parent.$el ? parent.$el.ownerDocument : document;
const settings: object = {
propsData: props,
parent,
data
};
if (!bulkComponentContainer) {
bulkComponentContainer = ownerDocument.createElement('DIV');
bulkComponentContainer.id = 'vueHotComponents';
ownerDocument.body.appendChild(bulkComponentContainer);
}
const componentContainer = ownerDocument.createElement('DIV');
bulkComponentContainer.appendChild(componentContainer);
return (new (vNode.componentOptions as any).Ctor(settings)).$mount(componentContainer);
}
/**
* Compare two objects using `JSON.stringify`.
* *Note: * As it's using the stringify function to compare objects, the property order in both objects is
* important. It will return `false` for the same objects, if they're defined in a different order.
*
* @param {object} objectA First object to compare.
* @param {object} objectB Second object to compare.
* @returns {boolean} `true` if they're the same, `false` otherwise.
*/
function simpleEqual(objectA, objectB) {
const stringifyToJSON = (val) => {
const circularReplacer = (function() {
const seen = new WeakSet();
return function(key, value) {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return;
seen.add(value);
}
return value;
};
}());
return JSON.stringify(val, circularReplacer);
};
if (typeof objectA === 'function' && typeof objectB === 'function') {
return objectA.toString() === objectB.toString();
} else if (typeof objectA !== typeof objectB) {
return false;
} else {
return stringifyToJSON(objectA) === stringifyToJSON(objectB);
}
}