合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
336
web_gantt/static/src/js/connector/connector.js
Normal file
336
web_gantt/static/src/js/connector/connector.js
Normal file
@@ -0,0 +1,336 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { deepMerge } from "./connector_utils";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class Connector extends Component {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Life cycle hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
this._refreshPropertiesFromProps(this.props);
|
||||
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @param nextProps
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async onWillUpdateProps(nextProps) {
|
||||
this._refreshPropertiesFromProps(nextProps);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Private
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Refreshes the connector properties from the props.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @private
|
||||
*/
|
||||
_refreshPropertiesFromProps(props) {
|
||||
const defaultStyleProps = {
|
||||
drawHead: true,
|
||||
outlineStroke: {
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
hoveredColor: 'rgba(255,255,255,0.9)',
|
||||
width: 2,
|
||||
},
|
||||
slackness: 0.5,
|
||||
stroke: {
|
||||
color: 'rgba(0,0,0,0.5)',
|
||||
hoveredColor: 'rgba(0,0,0,0.9)',
|
||||
width: 2,
|
||||
},
|
||||
};
|
||||
this.hoverEaseWidth = props.hoverEaseWidth ? props.hoverEaseWidth : 1;
|
||||
this.style = deepMerge(defaultStyleProps, props.style);
|
||||
const pathInfo = this._getPathInfo(props.source, props.target, this.style.slackness);
|
||||
this.path = `M ${pathInfo.singlePath.source.left} ${pathInfo.singlePath.source.top} \
|
||||
C ${pathInfo.singlePath.sourceControlPoint.left} ${pathInfo.singlePath.sourceControlPoint.top} \
|
||||
${pathInfo.singlePath.targetControlPoint.left} ${pathInfo.singlePath.targetControlPoint.top} \
|
||||
${pathInfo.singlePath.target.left} ${pathInfo.singlePath.target.top}`;
|
||||
this.removeButtonPosition = pathInfo.doublePath.startingPath.target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters of both the single Bezier curve as well as is decomposition into two beziers curves
|
||||
* (which allows to get the middle position of the single Bezier curve) for the provided source, target and
|
||||
* slackness (0 being a straight line).
|
||||
*
|
||||
* @param {{ left: number, top: number }} source
|
||||
* @param {{ left: number, top: number }} target
|
||||
* @param {number} slackness [0, 1]
|
||||
* @returns {{
|
||||
* singlePath: {
|
||||
* source: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* sourceControlPoint: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* target: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* targetControlPoint: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* }
|
||||
* },
|
||||
* doublePath: {
|
||||
* endingPath: {
|
||||
* source: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* sourceControlPoint: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* target: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* targetControlPoint: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* }
|
||||
* },
|
||||
* startingPath: {
|
||||
* source: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* sourceControlPoint: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* target: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* },
|
||||
* targetControlPoint: {
|
||||
* top: number,
|
||||
* left: number
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
_getPathInfo(source, target, slackness) {
|
||||
const b = { left: 0, top: 0 };
|
||||
const c = { left: 0, top: 0 };
|
||||
// If the source is on the left of the target, we need to invert the control points.
|
||||
const directionFactor = source.left < target.left ? 1 : -1;
|
||||
// What follows can be seen as magic numbers. And those are indeed such numbers as they have been determined
|
||||
// by observing their shape while creating short and long connectors. These seems to allow keeping the same
|
||||
// kind of shape amongst short and long connectors.
|
||||
const xDelta = 100 + directionFactor * (target.left - source.left) * slackness / 10;
|
||||
const yDelta = Math.abs(source.top - target.top) < 16 && source.left > target.left ? 15 + 0.001 * (source.left - target.left) * slackness : 0;
|
||||
b.left = source.left + xDelta;
|
||||
b.top = source.top + yDelta;
|
||||
// Prevent having the air pin effect when in creation and having target on the left of the source
|
||||
if (!this.props.inCreation || directionFactor > 0) {
|
||||
c.left = target.left - xDelta;
|
||||
} else {
|
||||
c.left = target.left + xDelta;
|
||||
}
|
||||
c.top = target.top + yDelta;
|
||||
|
||||
const cuttingDistance = 0.5;
|
||||
const e = Connector._getLinearInterpolation(source, b, cuttingDistance);
|
||||
const f = Connector._getLinearInterpolation(b, c, cuttingDistance);
|
||||
const g = Connector._getLinearInterpolation(c, target, cuttingDistance);
|
||||
const h = Connector._getLinearInterpolation(e, f, cuttingDistance);
|
||||
const i = Connector._getLinearInterpolation(f, g, cuttingDistance);
|
||||
const j = Connector._getLinearInterpolation(h, i, cuttingDistance);
|
||||
|
||||
return {
|
||||
singlePath: {
|
||||
source: source,
|
||||
sourceControlPoint: b,
|
||||
target: target,
|
||||
targetControlPoint: c,
|
||||
},
|
||||
doublePath: {
|
||||
endingPath: {
|
||||
source: j,
|
||||
sourceControlPoint: i,
|
||||
target: target,
|
||||
targetControlPoint: g,
|
||||
},
|
||||
startingPath: {
|
||||
source: source,
|
||||
sourceControlPoint: e,
|
||||
target: j,
|
||||
targetControlPoint: h,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Handlers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handler for connector_stroke_buttons remove click event.
|
||||
*
|
||||
* @param {OwlEvent} ev
|
||||
*/
|
||||
_onRemoveButtonClick(ev) {
|
||||
const payload = {
|
||||
data: deepMerge(this.props.data),
|
||||
id: this.props.id,
|
||||
};
|
||||
this.props.onRemoveButtonClick(payload);
|
||||
}
|
||||
/**
|
||||
* Handler for connector_stroke_buttons reschedule sooner click event.
|
||||
*
|
||||
* @param {OwlEvent} ev
|
||||
*/
|
||||
_onRescheduleSoonerClick(ev) {
|
||||
const payload = {
|
||||
data: deepMerge(this.props.data),
|
||||
id: this.props.id,
|
||||
};
|
||||
this.props.onRescheduleSoonerButtonClick(payload);
|
||||
}
|
||||
/**
|
||||
* Handler for connector_stroke_buttons reschedule later click event.
|
||||
*
|
||||
* @param {OwlEvent} ev
|
||||
*/
|
||||
_onRescheduleLaterClick(ev) {
|
||||
const payload = {
|
||||
data: deepMerge(this.props.data),
|
||||
id: this.props.id,
|
||||
};
|
||||
this.props.onRescheduleLaterButtonClick(payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const endProps = {
|
||||
shape: {
|
||||
left: Number,
|
||||
top: Number,
|
||||
},
|
||||
type: Object,
|
||||
};
|
||||
const strokeStyleProps = {
|
||||
optional: true,
|
||||
shape: {
|
||||
color: {
|
||||
optional: true,
|
||||
type: String,
|
||||
},
|
||||
hoveredColor: {
|
||||
optional: true,
|
||||
type: String,
|
||||
},
|
||||
width: {
|
||||
optional: true,
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
type: Object,
|
||||
};
|
||||
|
||||
Object.assign(Connector, {
|
||||
props: {
|
||||
canBeRemoved: {
|
||||
optional: true,
|
||||
type: Boolean
|
||||
},
|
||||
data: {
|
||||
optional: true,
|
||||
type: Object,
|
||||
},
|
||||
hoverEaseWidth: {
|
||||
optional: true,
|
||||
type: Number,
|
||||
},
|
||||
hovered: {
|
||||
optional: true,
|
||||
type: Boolean
|
||||
},
|
||||
inCreation: {
|
||||
optional: true,
|
||||
type: Boolean,
|
||||
},
|
||||
id: { type: String | Number },
|
||||
source: endProps,
|
||||
style: {
|
||||
optional: true,
|
||||
shape: {
|
||||
drawHead: {
|
||||
optional: true,
|
||||
type: Boolean,
|
||||
},
|
||||
outlineStroke: strokeStyleProps,
|
||||
slackness: {
|
||||
optional: true,
|
||||
type: Number,
|
||||
validate: slackness => (0 <= slackness && slackness <= 1),
|
||||
},
|
||||
stroke: strokeStyleProps,
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
target: endProps,
|
||||
onRemoveButtonClick: { type: Function, optional: true },
|
||||
onRescheduleSoonerButtonClick: { type: Function, optional: true },
|
||||
onRescheduleLaterButtonClick: { type: Function, optional: true },
|
||||
},
|
||||
defaultProps: {
|
||||
onRemoveButtonClick: () => {},
|
||||
onRescheduleSoonerButtonClick: () => {},
|
||||
onRescheduleLaterButtonClick: () => {},
|
||||
},
|
||||
template: 'connector',
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Private Static
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the linear interpolation for a point to be found somewhere between a startingPoint and a endingPoint.
|
||||
*
|
||||
* @param {{top: number, left: number}} startingPoint
|
||||
* @param {{top: number, left: number}} endingPoint
|
||||
* @param {number} interpolationPercentage [0, 1] the distance (from 0 startingPoint to 1 endingPoint)
|
||||
* the point has to be computed at.
|
||||
* @returns {{top: number, left: number}}
|
||||
* @private
|
||||
*/
|
||||
_getLinearInterpolation(startingPoint, endingPoint, interpolationPercentage) {
|
||||
if (interpolationPercentage < 0 || 1 > interpolationPercentage) {
|
||||
// Ensures interpolationPercentage is within expected boundaries.
|
||||
interpolationPercentage = Math.min(Math.max (0, interpolationPercentage), 1);
|
||||
}
|
||||
const remaining = 1 - interpolationPercentage;
|
||||
return {
|
||||
left: startingPoint.left * remaining + endingPoint.left * interpolationPercentage,
|
||||
top: startingPoint.top * remaining + endingPoint.top * interpolationPercentage
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default Connector;
|
||||
414
web_gantt/static/src/js/connector/connector_container.js
Normal file
414
web_gantt/static/src/js/connector/connector_container.js
Normal file
@@ -0,0 +1,414 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import Connector from "./connector";
|
||||
import { deepMerge } from "./connector_utils";
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
const { onMounted, onWillUnmount, onWillUpdateProps } = owl;
|
||||
|
||||
class ConnectorContainer extends LegacyComponent {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Life cycle hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
this._createsParentListenerHandlers();
|
||||
/**
|
||||
* Keeps track of the mouse events related info.
|
||||
* @type {{ isParentDragging: boolean, hoveredConnector: HTMLElement }}
|
||||
*/
|
||||
this.mouseEventsInfo = { };
|
||||
// Connector component used in order to manage connector creation.
|
||||
this.newConnector = {
|
||||
id: "newConnectorId",
|
||||
canBeRemoved: false,
|
||||
};
|
||||
// Apply default styling (if any) to newConnector.
|
||||
if ('defaultStyle' in this.props) {
|
||||
this.newConnector = deepMerge(this.newConnector, { style: this.props.defaultStyle });
|
||||
}
|
||||
if ('newConnectorStyle' in this.props) {
|
||||
this.newConnector = deepMerge(this.newConnector, { style: this.props.newConnectorStyle });
|
||||
}
|
||||
this._refreshPropertiesFromProps(this.props);
|
||||
|
||||
onMounted(this.onMounted);
|
||||
onWillUnmount(this.onWillUnmount);
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
onMounted() {
|
||||
if (this.parentElement && this.parentElement !== this.el.parentElement) {
|
||||
this._removeParentListeners();
|
||||
}
|
||||
this.parentElement = this.el.parentElement;
|
||||
this._addParentListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
onWillUnmount() {
|
||||
this._removeParentListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @param nextProps
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async onWillUpdateProps(nextProps) {
|
||||
this._refreshPropertiesFromProps(nextProps);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Private
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adds the ConnectorContainer's parent required EventListeners.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_addParentListeners() {
|
||||
this.parentElement.addEventListener('mousedown', this._onParentMouseDownHandler);
|
||||
this.parentElement.addEventListener('mousemove', this._throttledOnParentMouseOverHandler);
|
||||
this.parentElement.addEventListener('mouseup', this._onParentMouseUpHandler);
|
||||
this.parentElement.addEventListener('mouseleave', this._onParentMouseUpHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the handlers used in _addParentListeners and _removeParentListeners calls.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_createsParentListenerHandlers() {
|
||||
this._throttledOnParentMouseOverHandler = _.throttle((ev) => this._onParentMouseOver(ev), 50);
|
||||
this._onParentMouseDownHandler = (ev) => this._onParentMouseDown(ev);
|
||||
this._onParentMouseUpHandler = (ev) => this._onParentMouseUp(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the element offset against the connectorContainer's parent.
|
||||
*
|
||||
* @param {HTMLElement} element the element the offset position has to be calculated for.
|
||||
*/
|
||||
_getElementPosition(element) {
|
||||
// This will not work for css translated elements and this is acceptable at this time.
|
||||
// If needed in the future, consider using getBoundingClientRect() if getComputedStyle() returns a style
|
||||
// having a transform (and place this also in the for loop as we would need to do it if the element or any of
|
||||
// its parent is using css transform).
|
||||
let left = element.offsetLeft || 0;
|
||||
let top = element.offsetTop || 0;
|
||||
for (let el = element.offsetParent; el != null && el !== this.el.parentElement; el = el.offsetParent) {
|
||||
left += el.offsetLeft || 0;
|
||||
top += el.offsetTop || 0;
|
||||
}
|
||||
return {
|
||||
left: left,
|
||||
top: top,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the connector properties from the props.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @private
|
||||
*/
|
||||
_refreshPropertiesFromProps(props) {
|
||||
this.connectors = deepMerge(props.connectors);
|
||||
if (this.props.defaultStyle) {
|
||||
Object.keys(this.connectors)
|
||||
.forEach((key) => {
|
||||
this.connectors[key].style = deepMerge(this.props.defaultStyle, this.connectors[key].style);
|
||||
if (!('hoverEaseWidth' in this.connectors[key]) && 'hoverEaseWidth' in props) {
|
||||
this.connectors[key].hoverEaseWidth = props.hoverEaseWidth;
|
||||
}
|
||||
});
|
||||
}
|
||||
const isHoveredConnectorSet = 'hoveredConnector' in this.mouseEventsInfo;
|
||||
const isHoveredConnectorPartOfProps = isHoveredConnectorSet && this.mouseEventsInfo.hoveredConnector.dataset.id in this.connectors;
|
||||
if (isHoveredConnectorSet && !isHoveredConnectorPartOfProps) {
|
||||
// Ensures to reset the mouseEventsInfo in case the hoveredConnector is set but is no more part
|
||||
// of the new props.
|
||||
delete this.mouseEventsInfo.hoveredConnector;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the ConnectorContainer's parent required EventListeners.
|
||||
*
|
||||
*/
|
||||
_removeParentListeners() {
|
||||
this.parentElement.removeEventListener('mousedown', this._onParentMouseDownHandler);
|
||||
this.parentElement.removeEventListener('mousemove', this._throttledOnParentMouseOverHandler);
|
||||
this.parentElement.removeEventListener('mouseup', this._onParentMouseUpHandler);
|
||||
this.parentElement.removeEventListener('mouseleave', this._onParentMouseUpHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the hover state of the connector and render
|
||||
*
|
||||
* @param {string} id the id of the connector which hover state has to be updated.
|
||||
* @param {boolean} hovered the hover state to be set.
|
||||
* @private
|
||||
*/
|
||||
_updateConnectorHoverState(id, hovered) {
|
||||
this.connectors[id].hovered = hovered && !(this.props.preventHoverEffect || this.mouseEventsInfo.isParentDragging);
|
||||
if (hovered) {
|
||||
// When a connector is hover we need to ensure it is rendered as last element as svg z-index works
|
||||
// that way and unfortunately no css can be used to modify it.
|
||||
const hoverConnector = this.connectors[id];
|
||||
delete this.connectors[id];
|
||||
this.connectors[id] = hoverConnector;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Public
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets the top, right, bottom and left anchors positions for the provided element with respect to the
|
||||
* ConnectorContainer's parent.
|
||||
*
|
||||
* @param {HTMLElement} element the element the anchors positions have to be calculated for.
|
||||
* @param {HTMLElement} container the container the anchors positions will be calculated with respect to. In order
|
||||
* to have a valid result, the container should be an element with position attribute
|
||||
* set to relative.
|
||||
* @returns {{
|
||||
* top: {top: number, left: number},
|
||||
* left: {top: number, left: number},
|
||||
* bottom: {top: number, left: number},
|
||||
* right: {top: number, left: number}
|
||||
* }}
|
||||
*/
|
||||
getAnchorsPositions(element) {
|
||||
const elementPosition = this._getElementPosition(element);
|
||||
return {
|
||||
top: {
|
||||
top: elementPosition.top,
|
||||
left: elementPosition.left + element.offsetWidth / 2,
|
||||
},
|
||||
right: {
|
||||
top: elementPosition.top + element.offsetHeight / 2,
|
||||
left: elementPosition.left + element.offsetWidth,
|
||||
},
|
||||
bottom: {
|
||||
top: elementPosition.top + element.offsetHeight,
|
||||
left: elementPosition.left + element.offsetWidth / 2,
|
||||
},
|
||||
left: {
|
||||
top: elementPosition.top + element.offsetHeight / 2,
|
||||
left: elementPosition.left,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Handlers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handler for the ConnectorContainer's parent mousedown event. This handle is responsible of managing the start of a possible
|
||||
* connector creation (depending on whether the event target matches the sourceQuerySelector).
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
* @private
|
||||
*/
|
||||
_onParentMouseDown(ev) {
|
||||
const connector_source = ev.target.closest(this.props.sourceQuerySelector);
|
||||
if (connector_source) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.mouseEventsInfo.isParentDragging = true;
|
||||
const anchors = this.getAnchorsPositions(ev.target);
|
||||
this.newConnector = deepMerge(
|
||||
this.newConnector,
|
||||
{
|
||||
data: {
|
||||
sourceElement: connector_source,
|
||||
},
|
||||
inCreation: true,
|
||||
source: {
|
||||
top: anchors.right.top,
|
||||
left: anchors.right.left,
|
||||
},
|
||||
target: {
|
||||
top: anchors.right.top + ev.offsetY,
|
||||
left: anchors.right.left + ev.offsetX,
|
||||
},
|
||||
});
|
||||
this.props.onCreationStart(deepMerge({ }, this.newConnector));
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the ConnectorContainer's parent mouseover event. This handle is responsible of the update of the newConnector
|
||||
* component props if a connector creation has started.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
* @private
|
||||
*/
|
||||
_onParentMouseOver(ev) {
|
||||
if (this.mouseEventsInfo.isParentDragging === true) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const position = this._getElementPosition(ev.target);
|
||||
this.newConnector = deepMerge(
|
||||
this.newConnector,
|
||||
{
|
||||
target: {
|
||||
top: position.top + ev.offsetY,
|
||||
left: position.left + ev.offsetX,
|
||||
},
|
||||
});
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the ConnectorContainer's parent mouseup event. This handle is responsible of triggering either the
|
||||
* connector-creation-done or connector-creation-abort (depending on whether the event target matches the
|
||||
* targetQuerySelector) if a connector creation has started.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
* @private
|
||||
*/
|
||||
_onParentMouseUp(ev) {
|
||||
if (this.mouseEventsInfo.isParentDragging === true) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const connector_target = ev.target.closest(this.props.targetQuerySelector || this.props.sourceQuerySelector);
|
||||
if (connector_target) {
|
||||
this.newConnector = deepMerge(
|
||||
this.newConnector,
|
||||
{
|
||||
data: {
|
||||
targetElement: connector_target,
|
||||
},
|
||||
});
|
||||
this.props.onCreationDone(deepMerge({ }, this.newConnector));
|
||||
} else {
|
||||
this.props.onCreationAbort(deepMerge({ }, this.newConnector));
|
||||
}
|
||||
this.mouseEventsInfo.isParentDragging = false;
|
||||
delete this.newConnector.source;
|
||||
delete this.newConnector.target;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the connector_manager svg mouseout event. Its purpose is to handle the hover state of the connectors.
|
||||
* It has been implemented here in order to manage it globally instead of in each connector (and thus limiting
|
||||
* the number of listeners).
|
||||
*
|
||||
* @param {OwlEvent} ev
|
||||
* @private
|
||||
*/
|
||||
_onMouseOut(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!('hoveredConnector' in this.mouseEventsInfo)) {
|
||||
// If hoverConnector is not set this means were not in a connector. So ignore it.
|
||||
return;
|
||||
}
|
||||
let relatedTarget = ev.relatedTarget;
|
||||
while (relatedTarget) {
|
||||
// Go up the parent chain
|
||||
if (relatedTarget === this.mouseEventsInfo.hoveredConnector) {
|
||||
// Check that we are still inside hoveredConnector.
|
||||
// If so it means it is a transition between child elements so ignore it.
|
||||
return;
|
||||
}
|
||||
relatedTarget = relatedTarget.parentElement;
|
||||
}
|
||||
this._updateConnectorHoverState(this.mouseEventsInfo.hoveredConnector.dataset.id, false);
|
||||
this.props.onMouseOut(this.connectors[this.mouseEventsInfo.hoveredConnector.dataset.id]);
|
||||
delete this.mouseEventsInfo.hoveredConnector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the connector_manager svg mouseover event. Its purpose is to handle the hover state of the connectors.
|
||||
* It has been implemented here in order to manage it globally instead of in each connector (and thus limiting
|
||||
* the number of listeners).
|
||||
*
|
||||
* @param {OwlEvent} ev
|
||||
* @private
|
||||
*/
|
||||
_onMouseOver(ev) {
|
||||
ev.stopPropagation();
|
||||
if ('hoveredConnector' in this.mouseEventsInfo) {
|
||||
// As mouseout is call prior to mouseover, if hoveredConnector is set this means
|
||||
// that we haven't left it. So it's a mouseover inside it.
|
||||
return;
|
||||
}
|
||||
let target = ev.target.closest('.o_connector');
|
||||
if (!target) {
|
||||
// We are not into a connector si ignore.
|
||||
return;
|
||||
}
|
||||
if (!(target.dataset.id in this.connectors) || Object.is(this.connectors[target.dataset.id], this.newConnector)) {
|
||||
// We ensure that the connector to hover is not this.newConnector
|
||||
return;
|
||||
}
|
||||
this.mouseEventsInfo.hoveredConnector = target;
|
||||
this._updateConnectorHoverState(target.dataset.id, true);
|
||||
this.props.onMouseOver(this.connectors[target.dataset.id]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ConnectorContainer, {
|
||||
components: { Connector },
|
||||
props: {
|
||||
connectors: { type: Object },
|
||||
defaultStyle: Connector.props.style,
|
||||
hoverEaseWidth: {
|
||||
optional: true,
|
||||
type: Number,
|
||||
},
|
||||
newConnectorStyle: Connector.props.style,
|
||||
preventHoverEffect: {
|
||||
optional: true,
|
||||
type: Boolean
|
||||
},
|
||||
sourceQuerySelector: { type: String },
|
||||
targetQuerySelector: {
|
||||
optional: true,
|
||||
type: String,
|
||||
},
|
||||
onRemoveButtonClick: { type: Function, optional: true },
|
||||
onRescheduleSoonerButtonClick: { type: Function, optional: true },
|
||||
onRescheduleLaterButtonClick: { type: Function, optional: true },
|
||||
onCreationAbort: { type: Function, optional: true },
|
||||
onCreationDone: { type: Function, optional: true },
|
||||
onCreationStart: { type: Function, optional: true },
|
||||
onMouseOut: { type: Function, optional: true },
|
||||
onMouseOver: { type: Function, optional: true },
|
||||
},
|
||||
defaultProps: {
|
||||
onRemoveButtonClick: () => {},
|
||||
onRescheduleSoonerButtonClick: () => {},
|
||||
onRescheduleLaterButtonClick: () => {},
|
||||
onCreationAbort: () => {},
|
||||
onCreationDone: () => {},
|
||||
onCreationStart: () => {},
|
||||
onMouseOut: () => {},
|
||||
onMouseOver: () => {},
|
||||
},
|
||||
template: 'connector_container',
|
||||
});
|
||||
|
||||
export default ConnectorContainer;
|
||||
127
web_gantt/static/src/js/connector/connector_utils.js
Normal file
127
web_gantt/static/src/js/connector/connector_utils.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Private
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Clones the value according to its type and returns it.
|
||||
*
|
||||
* @param value
|
||||
* @return {Object|Array|*} The cloned value.
|
||||
* @private
|
||||
*/
|
||||
function _clone(value) {
|
||||
if (_isPlainObject(value) || Array.isArray(value)) {
|
||||
return deepMerge(_getEmptyTarget(value), value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an empty value according to value's type.
|
||||
*
|
||||
* @param {Object | Array} value
|
||||
* @return {Object | Array} an empty Array or Object.
|
||||
* @private
|
||||
*/
|
||||
function _getEmptyTarget(value) {
|
||||
return Array.isArray(value) ? [] : { };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the provided argument is a plain object or not.
|
||||
*
|
||||
* @param {*} value
|
||||
* @returns {boolean} true if the provided argument is a plain object, false if not.
|
||||
*/
|
||||
function _isPlainObject(value) {
|
||||
if (typeof value == 'object' && value !== null) {
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
return proto === Object.prototype || proto === null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merges target and source arrays and returns the result.
|
||||
*
|
||||
* @param {Array} target
|
||||
* @param {Array} source
|
||||
* @return {Array} the result of the merge.
|
||||
* @private
|
||||
*/
|
||||
function _deepMergeArray(target, source) {
|
||||
return target.concat(source)
|
||||
.map((entry) => _clone(entry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merges target and source Objects and returns the result
|
||||
*
|
||||
* @param {Object} target
|
||||
* @param {Object} source
|
||||
* @return {Object} the result of the merge.
|
||||
*/
|
||||
function _mergeObject(target, source) {
|
||||
const destination = { };
|
||||
if (_isPlainObject(target)) {
|
||||
Object.keys(target).forEach((key) => {
|
||||
destination[key] = _clone(target[key]);
|
||||
});
|
||||
}
|
||||
Object.keys(source).forEach((key) => {
|
||||
if ((_isPlainObject(target) && key in target) && _isPlainObject(source[key])) {
|
||||
destination[key] = deepMerge(target[key], source[key]);
|
||||
} else {
|
||||
destination[key] = _clone(source[key]);
|
||||
}
|
||||
});
|
||||
return destination;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Public
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Deep merges target and source and returns the result.
|
||||
* This implementation has been added since vanilla JS is now preferred.
|
||||
* A deep copy is made of all the plain objects and arrays.
|
||||
* For the other type of objects (like HTMLElement, etc.), the reference is passed.
|
||||
*
|
||||
* @param {Object | Array} target
|
||||
* @param {Object | Array} [source] if source is undefined, target wil be set to an empty value of source type.
|
||||
* @returns {Object | Array} the result of the merge.
|
||||
*/
|
||||
export function deepMerge(target, source) {
|
||||
if (typeof source === 'undefined') {
|
||||
source = _getEmptyTarget(source);
|
||||
}
|
||||
|
||||
const isSourceAnArray = Array.isArray(source);
|
||||
const isTargetAnArray = Array.isArray(target);
|
||||
|
||||
if (isSourceAnArray !== isTargetAnArray) {
|
||||
return _clone(source);
|
||||
} else if (isSourceAnArray) {
|
||||
return _deepMergeArray(target, source);
|
||||
} else {
|
||||
return _mergeObject(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merges all the entries from the passed array and returns the result.
|
||||
*
|
||||
* @param {Array} array the elements to be merged together.
|
||||
* @return {Object | Array} the result of the merge.
|
||||
*/
|
||||
export function deepMergeAll(array) {
|
||||
if (!Array.isArray(array)) {
|
||||
throw new Error('deepmergeAll argument must be an Array.');
|
||||
}
|
||||
return array.reduce((accumulator, current) => deepMerge(accumulator, current), { });
|
||||
}
|
||||
Reference in New Issue
Block a user