ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
481 lines (438 loc) • 15.1 kB
JavaScript
/* jshint indent: false */ // TODO: ENYO-3311
enyo.kind({
name: "ComponentView",
events: {
onSelect: "",
onHighlightDropTarget: "",
onUnHighlightDropTargets: "",
onMoveItem: "",
onCreateItem: "",
onHoldOver: ""
},
handlers: {
onItemDown: "itemDown",
onItemDragover: "itemDragover",
onPlaceholderDrop: "placeholderDrop",
onItemDrop: "itemDrop",
onItemDragend: "itemDragend"
},
published: {
dragType: null
},
style: "position: relative;",
components: [
{kind: "Scroller", classes: "enyo-fit", components: [
{name: "client", style: "padding: 8px;"}
]}
],
holdoverTimeout: null,
holdoverTimeoutMS: 500,
scroll: false,
//* Draw component view visualization of component tree
visualize: function(inComponents) {
this.destroyClientControls();
this._visualize(inComponents, this);
this.render();
},
//* Create component view representation of designer
_visualize: function(inComponents, inParent) {
for (var i=0, entry, c; (c=inComponents[i]); i++) {
entry = inParent.createComponent(this.createEntry(c));
if (c.components) {
this._visualize(c.components, entry);
}
}
},
//* Create an entry in the component view
createEntry: function(inComponent) {
return {kind: "ComponentViewItem", comp: inComponent};
},
//* Unhighlight existing selection and set _this.selection_ to _inComponent_
select: function(inComponent) {
if(this.selection) {
this.unHighlightItem(this.selection);
}
this.selection = inComponent;
this.highlightSelection();
if(this.scroll){
this.$.scroller.scrollIntoView(this.selection, true);
}
this.scroll = false;
},
//* Select control with _comp.aresId_ that matches _inComponent_
setSelected: function(inComponent, haveToScroll) {
this.scroll = haveToScroll;
this._setSelected(inComponent.aresId, this.getClientControls());
},
_setSelected: function(inId, inComponents) {
for (var i=0, c; (c=inComponents[i]); i++) {
if(c.comp && c.getAresId() === inId) {
this.select(c);
return true;
}
if (c.getClientControls().length > 0) {
if(this._setSelected(inId, c.getClientControls())) {
return true;
}
}
}
},
itemDown: function(inSender, inEvent) {
this.doSelect({component: inEvent.originator.comp});
return true;
},
itemDragover: function(inSender, inEvent) {
var target = inEvent.targetComponent,
dropDetails,
dropTarget,
dropTargetId,
beforeTarget,
beforeItem,
beforeId = null,
createMode;
if (inEvent.dataTransfer.items) {
createMode = (this.getDragType() === "ares/createitem");
}
// In create mode, allow to drop even on the selected component.
if ((createMode === true) && this.selection) {
this.unHighlightItem(this.selection);
this.selection = null;
}
if (!this.isValidDropTarget(target)) {
this.resetDropDetails();
this.unhighlightDropTargets();
return true;
}
// Enable HTML5 drop
inEvent.preventDefault();
dropDetails = this.calcDropDetails(target, inEvent.clientY);
// No need to redo the work if the data hasn't changed
if (this.dropDetails && dropDetails.pos === this.dropDetails.pos && dropDetails.target === this.dropDetails.target) {
return true;
}
this.dropDetails = dropDetails;
dropTarget = dropDetails.target;
// Clear the placeholder on every dragover
this.destroyPlaceholder();
// Reset the holdover timeout on every dragover
this.resetHoldoverTimeout();
if (dropDetails.pos === "center") {
this.highlightDropTarget(dropTarget);
dropTargetId = dropTarget.comp.aresId;
} else {
this.unhighlightDropTargets();
this.createDropPlaceholder(dropTarget, dropDetails.pos);
// TODO - owner should always have a getAresId() function - currently fails if owner is the app.
dropTargetId = dropTarget.owner.getAresId ? dropTarget.owner.getAresId() : null;
beforeTarget = this.findControlBeforeTarget(dropTarget, dropDetails.pos);
beforeItem = beforeTarget ? beforeTarget.comp : null;
beforeId = beforeItem ? beforeItem.aresId : null;
}
this.setHoldoverTimeout(dropTargetId, beforeId);
return true;
},
//* When a component is dropped on another component
itemDrop: function(inSender, inEvent) {
this.destroyPlaceholder();
var dropData = this.getDropData(inEvent),
before,
containerId,
beforeId;
// If the item was dropped in the center, add dragged control to target's controls
if (this.dropDetails.pos === "center") {
this.completeDrop(dropData, this.dropDetails.target.getAresId(), null);
} else {
before = this.findControlBeforeTarget(this.dropDetails.target, this.dropDetails.pos);
// If dropping into the highest level control
if (this.dropDetails.target.owner === this) {
beforeId = (typeof before === "undefined" || before === null) ? null : before.getAresId();
this.completeDrop(dropData, null, beforeId);
// If _before_ === null, drop this item at the end of it's container (there is no before item)
} else if (before === null) {
this.completeDrop(dropData, this.dropDetails.target.owner.getAresId(), null);
// Otherwise drop current item before _before_
} else {
containerId = (this.dropDetails.target.owner === this) ? null : this.dropDetails.target.owner.getAresId();
this.completeDrop(dropData, containerId, before.getAresId());
}
}
return true;
},
itemDragend: function(inSender, inEvent) {
this.destroyPlaceholder();
},
//* When a component is dropped on a placeholder
placeholderDrop: function(inSender, inEvent) {
this.destroyPlaceholder();
var dropData = this.getDropData(inEvent),
containerId = (this.dropDetails.target.owner.comp) ? this.dropDetails.target.owner.getAresId() : null,
before = this.findControlBeforeTarget(this.dropDetails.target, this.dropDetails.pos),
beforeId = (before && before.comp) ? before.getAresId() : null;
this.completeDrop(dropData, containerId, beforeId);
return true;
},
getDropData: function(inEvent) {
var data = enyo.json.codify.from(inEvent.dataTransfer.getData("text")),
item = data.item,
type = data.type;
return {type: type, data: item};
},
completeDrop: function(inDropData, inTargetId, inBeforeId) {
switch (inDropData.type) {
case "ares/moveitem":
this.doMoveItem({
itemId: inDropData.data.aresId,
targetId: inTargetId,
beforeId: inBeforeId
});
break;
case "ares/createitem":
this.doCreateItem({
config: inDropData.data.config,
options: inDropData.data.options,
targetId: inTargetId,
beforeId: inBeforeId
});
break;
default:
enyo.warn("Component view received unknown drop: ", inDropData);
break;
}
this.resetDropDetails();
},
setHoldoverTimeout: function (inTargetId, inBeforeId) {
this.holdoverTimeout = setTimeout(enyo.bind(this, function() { this.holdOver(inTargetId, inBeforeId); }), this.holdoverTimeoutMS);
},
holdOver: function (inTargetId, inBeforeId) {
this.doHoldOver({targetId: inTargetId, beforeId: inBeforeId});
},
isValidDropTarget: function(inComponent) {
return (inComponent !== this.selection && inComponent.getAttribute("dropTarget") === "true" && !inComponent.isDescendantOf(this.selection));
},
calcDropDetails: function(inTarget, inClientY) {
if (!inTarget.hasNode()) {
return null;
}
var rect = inTarget.hasNode().getBoundingClientRect(),
topEdge = 5,
bottomEdge = rect.height - topEdge,
distanceFromTop = inClientY - rect.top,
pos = (distanceFromTop <= topEdge) ? "top" : (distanceFromTop >= bottomEdge) ? "bottom" : "center";
return {pos: pos, target: inTarget};
},
findControlFromDropDetails: function(inTarget, inPos) {
var targetIndex = this.indexOfControl(inTarget),
index = (inPos === "center") ? targetIndex : (inPos === "top") ? targetIndex-1 : targetIndex+1;
return this.controlAtIndex(index);
},
createDropPlaceholder: function(inTarget, inPos) {
var beforeControl = this.findControlBeforeTarget(inTarget, inPos),
leftMargin = (typeof beforeControl === "undefined")
? "0px"
: (beforeControl === null)
? inTarget.owner.domStyles["padding-left"] || "0px"
: beforeControl.domStyles["padding-left"] || "0px",
placeholder = {kind: "ComponentViewPlaceholder", style: "margin-left:" + leftMargin};
if (beforeControl) {
placeholder.addBefore = beforeControl;
}
this.dropPlaceholder = inTarget.owner.createComponent(placeholder);
this.dropPlaceholder.render();
},
findControlBeforeTarget: function(inTarget, inPos) {
var addBeforeIndex = this.calcAddBeforeIndex(inTarget, inPos);
return (addBeforeIndex === -1) ? null : inTarget.owner.controlAtIndex(addBeforeIndex);
},
calcAddBeforeIndex: function(inTarget, inPos) {
var owner = inTarget.owner,
targetIndex = owner.indexOfControl(inTarget),
addBeforeIndex = (inPos === "top") ? targetIndex : targetIndex + 1;
return (addBeforeIndex > owner.getClientControls().length + 1) ? -1 : addBeforeIndex;
},
destroyPlaceholder: function() {
if(this.dropPlaceholder) {
this.dropPlaceholder.destroy();
}
},
resetDropDetails: function() {
this.dropDetails = null;
this.setDragType(null);
},
resetHoldoverTimeout: function() {
clearTimeout(this.holdoverTimeout);
this.holdoverTimeout = null;
},
highlightDropTarget: function(inComponent) {
inComponent.$.label.applyStyle("background","#cedafe"); // TODO
this.doHighlightDropTarget({component: inComponent.comp});
},
highlightSelection: function() {
this.selection.$.label.applyStyle("background","orange"); // TODO
},
unHighlightItem: function(inComponent) {
if(inComponent.$.label) {
inComponent.$.label.applyStyle("background", "none"); // TODO
}
},
unhighlightDropTargets: function() {
this._unhighlightDropTargets(this.getClientControls());
this.doUnHighlightDropTargets();
},
_unhighlightDropTargets: function(inComponents) {
if (inComponents.length === 0) {
return;
}
for (var i=0, c; (c=inComponents[i]); i++) {
if(c === this.selection || c.kind === "ComponentViewPlaceholder") {
continue;
}
this.unHighlightItem(c);
this._unhighlightDropTargets(c.getClientControls());
}
},
syncDropTargetHighlighting: function(inComponent) {
var id = inComponent ? inComponent.aresId : null;
this._syncDropTargetHighlighting(id, this.getClientControls());
},
_syncDropTargetHighlighting: function(inId, inComponents) {
if (inComponents.length === 0) {
return;
}
for (var i=0, c; (c=inComponents[i]); i++) {
if(c === this.selection || !c.comp) {
continue;
} else if (c.getAresId() === inId) {
this.highlightDropTarget(c);
continue;
}
this.unHighlightItem(c);
this._syncDropTargetHighlighting(inId, c.getClientControls());
}
}
});
enyo.kind({
name: "ComponentViewItem",
published: {
comp: {}
},
events: {
onItemDown: "",
onItemDragover: "",
onItemDrop: "",
onItemDragend: ""
},
handlers: {
ondown: "down",
ondragstart: "dragstart",
ondragover: "dragover",
ondragleave: "dragleave",
ondrop: "drop",
ondragend: "dragend"
},
style: "padding-left: 15px;",
attributes: {
dropTarget: "true"
},
components: [
{name: "label", components: [
{name: "componentName", tag: "b", style: "pointer-events: none; line-height: 20px;"},
{name: "componentKind", tag: "span", allowHtml: true, style: "pointer-events: none; line-height: 20px;"}
]},
{name: "client", style: "position: relative;"},
{name: "dragTarget", attributes: {draggable: "true"}, comp: null, style: "background-color: white; position: absolute; opacity: 0.1;"}
],
rendered: function() {
this.inherited(arguments);
this.compChanged();
this.$.dragTarget.setBounds(this.$.label.getBounds());
},
compChanged: function() {
this.$.dragTarget.comp = this.getComp();
this.$.componentName.setContent(this.getComp().name);
this.$.componentKind.setContent(" (<i>"+this.getComp().kind+"</i>)");
},
getAresId: function() {
return this.getComp().aresId;
},
down: function(inSender, inEvent) {
this.doItemDown(inEvent);
// Using encoded 1px x 1px transparent png
var imageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjEwRDRFRUY1Rjk3NDExRTI5NTRFQ0U1RjAwMURENDczIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjEwRDRFRUY2Rjk3NDExRTI5NTRFQ0U1RjAwMURENDczIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTBENEVFRjNGOTc0MTFFMjk1NEVDRTVGMDAxREQ0NzMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MTBENEVFRjRGOTc0MTFFMjk1NEVDRTVGMDAxREQ0NzMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4/IH2ZAAAAEElEQVR42mL4//8/A0CAAQAI/AL+26JNFgAAAABJRU5ErkJggg==";
this.createDragImage(imageData);
return true;
},
dragstart: function(inSender, inEvent) {
if(!inEvent.dataTransfer) {
return true;
}
var dragData = {
type: "ares/moveitem",
item: inEvent.originator.comp
};
// Set drag data
inEvent.dataTransfer.setData("text", enyo.json.codify.to(dragData));
// Hide the drag image ghost on platforms where it exists
if (inEvent.dataTransfer.setDragImage) {
inEvent.dataTransfer.setDragImage(this.dragImage, 0, 0);
}
return true;
},
dragover: function(inSender, inEvent) {
if (!inEvent.dataTransfer) {
return true;
}
inEvent.targetComponent = this;
this.doItemDragover(inEvent);
return true;
},
dragleave: function(inSender, inEvent) {
if (!inEvent.dataTransfer) {
return true;
}
},
drop: function(inSender, inEvent) {
if (!inEvent.dataTransfer) {
return true;
}
this.doItemDrop(inEvent);
return true;
},
dragend: function(inSender, inEvent) {
if (!inEvent.dataTransfer) {
return true;
}
this.doItemDragend(inEvent);
return true;
},
createDragImage: function(inImage) {
this.dragImage = document.createElement("img");
this.dragImage.src = inImage;
return this.dragImage;
}
});
enyo.kind({
name: "ComponentViewPlaceholder",
classes: "component-view-drop-placeholder",
published: {
before: null
},
handlers: {
ondragover: "dragover",
ondragleave: "dragleave",
ondrop: "drop"
},
events: {
onPlaceholderDrop: ""
},
dragover: function(inSender, inEvent) {
inEvent.preventDefault();
return true;
},
dragleave: function() {
return true;
},
drop: function(inSender, inEvent) {
this.doPlaceholderDrop(inEvent);
return true;
}
});