UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

1,497 lines (1,340 loc) 61.8 kB
/*global enyo, setTimeout, clearTimeout */ enyo.kind({ name: "Ares.DesignerFrame", classes: "enyo-fit", id: "aresApp", handlers: { ondragleave: "designerFrameDragleave", onWebkitTransitionEnd: "prerenderMoveComplete" // TODO }, published: { containerItem: null, beforeItem: null, currentDropTarget: null, createPaletteItem: null, dragType: null }, components: [ {name: "client", classes:"enyo-fit"}, {name: "cloneArea", style: "background:rgba(0,200,0,0.5); opacity: 0;", classes: "enyo-fit enyo-clip", showing: false}, {name: "flightArea", classes: "enyo-fit", showing: false}, {name: "serializer", kind: "Ares.Serializer"}, {name: "communicator", kind: "RPCCommunicator", onMessage: "receiveMessage"}, {name: "dropHighlight", classes: "designer-frame-highlight designer-frame-drop-highlight"}, //* Resize handles {name: "topLeftResizeHandle", classes: "designer-frame-resize-handle", showing: false, sides: {top: true, left: true}, style: "top: 0px; left: 0px;"}, {name: "topRightResizeHandle", classes: "designer-frame-resize-handle", showing: false, sides: {top: true, right: true}, style: "top: 0px; right: 0px;"}, {name: "bottomLeftResizeHandle", classes: "designer-frame-resize-handle", showing: false, sides: {bottom: true, left: true}, style: "bottom: 0px; left: 0px;"}, {name: "bottomRightResizeHandle", classes: "designer-frame-resize-handle", showing: false, sides: {bottom: true, right: true}, style: "bottom: 0px; right: 0px;"} ], selection: null, parentInstance: null, containerData: null, aresComponents: [], prevX: null, prevY: null, dragoverTimeout: null, holdoverTimeout: null, moveControlSecs: 0.2, edgeThresholdPx: 10, debug: false, // designerFrame will complain if user's application loads en enyo older than: minEnyoVersion: "2.3.0-pre.9", create: function() { this.trace = (this.debug === true ? this.log : function(){}); this.inherited(arguments); this.addHandlers(); this.addDispatcherFeature(); window.onerror = enyo.bind(this, this.raiseLoadError); }, raiseLoadError: function(msg, url, linenumber) { // I'm a goner var file = url.replace(/.*\/services(?=\/)/,''); var errMsg = "user app load FAILED with error '" + msg + "' in " + file + " line " + linenumber ; this.trace(errMsg); this.sendMessage({op: "reloadNeeded"}); this.sendMessage({op: "error", val: {msg: errMsg}}); return true; }, rendered: function() { this.inherited(arguments); this.trace("called"); var splitRegExp = /[\.\-]+/ ; // version can be like 1.2.3-pre.4 or 1.2.3-rc.1 // rc sorts after pre var expVer = this.minEnyoVersion.split(splitRegExp); var myVerStr = (enyo.version && enyo.version.enyo) || '0.0.0-pre.0'; var myVer = myVerStr.split(splitRegExp); var errMsg, mysv, expsv ; while (expVer.length) { mysv = myVer.shift(); expsv = expVer.shift(); // myVer and exptVer contain a string. Need to cast them // to Number before trying a numeric comparison. Otherwise // a lexicographic comparison is used. if (/\d/.test(mysv) ? Number(mysv) > Number(expsv) : mysv > expsv) { // found a greater version, no need to compare lower fields break; } if (/\d/.test(mysv) ? Number(mysv) < Number(expsv) : mysv < expsv) { errMsg = "Enyo used by your application is too old (" + myVerStr + "). Console log may show duplicated kind error " + "and Designer may not work as expected. You should use Enyo >= " + this.minEnyoVersion+" Read <a href='https://github.com/enyojs/ares-project/blob/master/README.md' target='_blank'>README.md to update Enyo libraries</a>"; enyo.warn(errMsg); /* * TODO this message should go in an error/warning history as described in ENYO-2462 * Un-commenting the following "sendMessage" call will result in a annoying modal message * popping up too often. For the time being, we just issue a warning in the console. * * this.sendMessage({op: "error", val: {msg: errMsg, title: "warning"}}); */ break; } } this.adjustFrameworkFeatures(); this.trace("designer iframe load done"); this.sendMessage({op: "state", val: "loaded"}); }, initComponents: function() { this.createSelectHighlight(); this.inherited(arguments); }, createSelectHighlight: function() { var components = [{name: "selectHighlight", classes: "designer-frame-highlight designer-frame-select-highlight", showing: false}]; // IE can only support pointer-events:none; for svg elements if (enyo.platform.ie) { // Using svg for IE only as it causes performance issues in Chrome components[0].tag = "svg"; // Unable to retrive offset values for svg elements in IE, thus we're forced to create additional dom for resizeHandle calculations components.push({name: "selectHighlightCopy", classes: "designer-frame-highlight", style: "z-index:-1;", showing: false}); } this.createComponents(components); }, //* Any core features of the framework that need to be overridden/diesabled happens here adjustFrameworkFeatures: function() { // Allow overriding kind definitions enyo.kind.allowOverride = true; // Disable autoStart/autoRender features of enyo.Application if (enyo.Application) { enyo.Application.prototype.start = enyo.nop; enyo.Application.prototype.render = enyo.nop; } // Disable controller instancing enyo.Control.prototype.controllerFindAndInstance = enyo.nop; }, currentDropTargetChanged: function() { if (this.getCurrentDropTarget()) { this.highlightDropTarget(this.getCurrentDropTarget()); } this.syncDropTargetHighlighting(); }, //* Add dispatch handling for native drag events addHandlers: function(inSender, inEvent) { document.ondragstart = enyo.dispatch; document.ondrag = enyo.dispatch; document.ondragenter = enyo.dispatch; document.ondragleave = enyo.dispatch; document.ondragover = enyo.dispatch; document.ondrop = enyo.dispatch; document.ondragend = enyo.dispatch; }, /** Add feature to dispatcher to catch drag-and-drop-related events, and to stop any/all DOM events from being handled by the app. */ addDispatcherFeature: function() { var _this = this; enyo.dispatcher.features.push( function(e) { if (_this[e.type]) { _this[e.type](e); } e.preventDispatch = true; return true; } ); }, //* Send message to Deimos via _this.$.communicator_ sendMessage: function(inMessage) { this.trace(" msg ",inMessage); this.$.communicator.sendMessage(inMessage); }, //* Receive message from Deimos receiveMessage: function(inSender, inEvent) { var msg = inEvent.message; this.trace(" msg ",msg); if (!msg || !msg.op) { enyo.warn("Deimos designerFrame received invalid message data:", msg); return; } switch (msg.op) { case "containerData": this.setContainerData(msg.val); break; case "render": this.renderKind(msg.val, msg.filename, msg.op); break; case "initializeOptions": this.initializeAllKindsAresOptions(msg.options); break; case "select": this.selectItem(msg.val); break; case "highlight": this.highlightDropTarget(this.getControlById(msg.val.aresId)); break; case "unhighlight": this.unhighlightDropTargets(msg.val); break; case "modify": this.modifyProperty(msg.val, msg.filename); break; case "codeUpdate": this.codeUpdate(msg.val); break; case "cssUpdate": this.cssUpdate(msg.val); break; case "cleanUp": this.cleanUpKind(); break; case "resize": this.resized(); break; case "prerenderDrop": this.foreignPrerenderDrop(msg.val); break; case "requestPositionValue": this.requestPositionValue(msg.val); break; case "serializerOptions": this.$.serializer.setSerializerOptions(msg.val); break; case "dragStart": this.setDragType(msg.val); break; default: enyo.warn("Deimos designerFrame received unknown message op:", msg); break; } }, //* On down, set _this.selection_ down: function(e) { var dragTarget = this.getEventDragTarget(e.dispatchTarget); if (dragTarget && dragTarget.aresComponent) { this._selectItem(dragTarget); } // 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); }, up: function(e) { this.clearDragImage(); }, //* On drag start, set the event _dataTransfer_ property to contain a serialized copy of _this.selection_ dragstart: function(e) { if (!e.dataTransfer) { this.resizing = this.getActiveResizingHandle(e); return false; } var dragData = { type: "ares/moveitem", item: enyo.json.codify.from(this.$.serializer.serializeComponent(this.selection, true)) }; // Set drag data e.dataTransfer.setData("text", enyo.json.codify.to(dragData)); // Hide the drag image ghost on platforms where it exists if (e.dataTransfer.setDragImage) { e.dataTransfer.setDragImage(this.dragImage, 0, 0); } return true; }, //* On drag over, enable HTML5 drag-and-drop if there is a valid drop target dragover: function(inEvent) { if (!inEvent.dataTransfer) { return false; } // Enable HTML5 drag-and-drop inEvent.preventDefault(); // Update dragover highlighting this.dragoverHighlighting(inEvent); // Don't do holdover if item is being dragged in from the palette if (this.getDragType() === "ares/createitem") { return true; } // If dragging in an absolute positioning container, go straight to _holdOver()_ if (this.absolutePositioningMode(this.getCurrentDropTarget())) { this.holdOver(inEvent); // If mouse actually moved, begin timer for holdover } else if (this.mouseMoved(inEvent)) { this.resetHoldoverTimeout(); // If mouse hasn't moved and timer isn't yet set, set it } else if (!this.holdoverTimeout) { this.holdoverTimeout = setTimeout(enyo.bind(this, function() { this.holdOver(inEvent); }), 200); } // Remember mouse location for next dragover event this.saveMouseLocation(inEvent); return true; }, drag: function(inEvent) { if (this.resizing) { this.resizeOnEvent(inEvent); } }, dragenter: function(inEvent) { // Enable HTML5 drag-and-drop inEvent.preventDefault(); }, //* On drag leave, unhighlight previous drop target dragleave: function(inEvent) { var dropTarget; if (!inEvent.dataTransfer) { return false; } dropTarget = this.getEventDropTarget(inEvent.dispatchTarget); if (!this.isValidDropTarget(dropTarget)) { return false; } this.setCurrentDropTarget(null); this.syncDropTargetHighlighting(); return true; }, //* On drop, either move _this.selection_ or create a new component drop: function(inEvent) { if (!inEvent.dataTransfer) { if (this.resizing) { this.resizeComplete(); } return true; } var dropData = enyo.json.codify.from(inEvent.dataTransfer.getData("text")), dropItem = dropData.item, dataType = dropData.type, dropTargetId, dropTarget = this.getEventDropTarget(inEvent.dispatchTarget), beforeId ; switch (dataType) { case "ares/moveitem": dropTargetId = (dropTarget) ? dropTarget.aresId : this.selection.parent.aresId; beforeId = this.selection.addBefore ? this.selection.addBefore.aresId : null; this.sendMessage({op: "moveItem", val: {itemId: dropItem.aresId, targetId: dropTargetId, beforeId: beforeId, layoutData: this.getLayoutData(inEvent)}}); break; case "ares/createitem": dropTargetId = this.getContainerItem() ? this.getContainerItem() : dropTarget; dropTargetId = (dropTargetId && dropTargetId.aresId) || null; beforeId = this.getBeforeItem() ? this.getBeforeItem().aresId : null; this.sendMessage({op: "createItem", val: {config: dropData.item.config, options: dropData.item.options, targetId: dropTargetId, beforeId: beforeId}}); break; default: enyo.warn("Component view received unknown drop: ", dataType, dropData); break; } this.setContainerItem(null); this.setBeforeItem(null); this.setDragType(null); return true; }, dragend: function() { this.setCurrentDropTarget(null); this.syncDropTargetHighlighting(); this.unhighlightDropTargets(); this.clearDragImage(); }, createDragImage: function(inImage) { this.dragImage = document.createElement("img"); this.dragImage.src = inImage; return this.dragImage; }, clearDragImage: function() { this.dragImage = null; }, resetHoldoverTimeout: function() { clearTimeout(this.holdoverTimeout); this.holdoverTimeout = null; if (this.selection && this.selection.addBefore) { this.resetAddBefore(); } }, //* Reset the control currently set as _addBefore_ on _this.selection_ resetAddBefore: function() { this.selection.addBefore = null; }, mouseMoved: function(inEvent) { return (this.prevX !== inEvent.clientX || this.prevY !== inEvent.clientY); }, saveMouseLocation: function(inEvent) { this.prevX = inEvent.clientX; this.prevY = inEvent.clientY; }, dragoverHighlighting: function(inEvent) { var dropTarget = this.getEventDropTarget(inEvent.dispatchTarget); // Deselect the currently selected item if we're creating a new item, so all items are droppable if (this.getDragType() === "ares/createitem") { this.selection = null; this.hideSelectHighlight(); } // If not a valid drop target, reset _this.currentDropTarget_ if (!this.isValidDropTarget(dropTarget)) { this.setCurrentDropTarget(null); return false; } // If drop target has changed, update drop target highlighting if (!(this.currentDropTarget && this.currentDropTarget === dropTarget)) { this.setCurrentDropTarget(dropTarget); } }, //* Save _inData_ as _this.containerData_ to use as a reference when creating drop targets. setContainerData: function(inData) { this.containerData = inData; this.sendMessage({op: "state", val: "ready"}); }, //* Render the specified kind renderKind: function(inKind, inFileName, cmd) { var errMsg; try { var kindConstructor = enyo.constructorForKind(inKind.name); if (!kindConstructor) { errMsg = "No constructor exists for "; enyo.warn(errMsg, inKind.name); this.sendMessage({op: "error", val: {triggeredByOp: cmd, msg: errMsg + inKind.name}}); return; } else if(!kindConstructor.prototype) { errMsg = "No prototype exists for "; enyo.warn(errMsg, inKind.name); this.sendMessage({op: "error", val: {triggeredByOp: cmd, msg: errMsg + inKind.name}}); return; } /* Stomp on existing _kindComponents_ to ensure that we render exactly what the user has defined. If components came in as a string, convert to object first. */ kindConstructor.prototype.kindComponents = (typeof inKind.components === "string") ? enyo.json.codify.from(inKind.componentKinds) : inKind.componentKinds; // Clean up after previous kind if (this.parentInstance) { this.cleanUpPreviousKind(inKind.name); } // Proxy Repeater and List this.manageComponentsOptions(kindConstructor.prototype.kindComponents); // Save this kind's _kindComponents_ array this.aresComponents = this.flattenKindComponents(kindConstructor.prototype.kindComponents); this.checkXtorForAllKinds(this.aresComponents); // Enable drag/drop on all of _this.aresComponents_ this.makeComponentsDragAndDrop(this.aresComponents); // Save reference to the parent instance currently rendered this.parentInstance = this.$.client.createComponent({kind: inKind.name}); // Mimic top-level app fitting (as if this was rendered with renderInto or write) if (this.parentInstance.fit) { this.parentInstance.addClass("enyo-fit enyo-clip"); } this.parentInstance.domStyles = {}; this.parentInstance.domStylesChanged(); this.parentInstance.render(); // Notify Deimos that the kind rendered successfully //* Send update to Deimos with serialized copy of current kind component structure this.sendMessage({ op: "rendered", triggeredByOp: cmd, // 'render' val: this.$.serializer.serialize(this.parentInstance, true), filename: inFileName }); // Select a control if so requested if (inKind.selectId) { this.selectItem({aresId: inKind.selectId}); } } catch(error) { errMsg = "Unable to render kind '" + inKind.name + "':" + ( typeof error === 'object' ? error.message : error ); var errStack = typeof error === 'object' ? error.stack : '' ; this.error(errMsg, errStack ); this.sendMessage({op: "reloadNeeded"}); this.sendMessage({op: "error", val: {msg: errMsg, triggeredByOp: cmd, requestReload: true, err: {stack: errStack}}}); } }, //* Rerender current selection rerenderKind: function(inFileName) { var copy = this.getSerializedCopyOfComponent(this.parentInstance).components; copy[0].componentKinds = copy; this.renderKind(copy[0], inFileName); }, checkXtorForAllKinds: function(kinds) { enyo.forEach(kinds, function(kindDefinition) { var name = kindDefinition.kind; if ( ! enyo.constructorForKind(name)) { var errMsg = 'No constructor found for kind "' + name + "'"; this.log(errMsg); this.sendMessage({op: "error", val: {msg: errMsg}}); } }, this); }, /** * @private * * Response to message sent from Deimos. Enhance the whole application code with aresOptions * and send back a state message. */ initializeAllKindsAresOptions: function(inOptions) { // genuine enyo.kind's master function extension var self = this; this.trace("starting user app initialization within designer iframe"); enyo.genuineEnyoKind = enyo.kind; enyo.kind = function(inProps) { self.addKindAresOptions(inProps.components, inOptions); enyo.genuineEnyoKind(inProps); }; enyo.mixin(enyo.kind, enyo.genuineEnyoKind); // warning: user code error will trigger this.raiseLoadError // through window.onerror handler // another warning: enyo.load is asynchronous. try/catch is useless enyo.load("$enyo/../source/package.js", enyo.bind(this, function() { this.trace("user app initialization done within designer iframe"); this.sendMessage({op: "state", val: "initialized"}); })); }, addKindAresOptions: function(inComponents, inOptions) { if (!inComponents) { return; } for(var i = 0; i < inComponents.length; i++) { this.addAresOptionsToComponent(inComponents[i], inOptions); if (inComponents[i].components) { this.addKindAresOptions(inComponents[i].components, inOptions); } } }, addAresOptionsToComponent: function(inComponent, inOptions) { // FIXME: ENYO-3433 specific enyo.Repeater create method must be generic one accordingly to kinds that require options function aresOptionCreate() { if (this.__create) { this.__create(); } // for enyo.Repeater (currently only kind in defaultkindOptions set) if (this.__aresOptions.isRepeater === true) { this.onSetupItem = "aresUnImplemetedFunction"; this.set("count", 1); } } for(var o in inOptions) { if (o === inComponent.kind) { var options = inOptions[o]; if (options) { inComponent.__aresOptions = options; var kindConstructor= enyo.constructorForKind(inComponent.kind); if (kindConstructor.prototype.__create) { this.trace(inComponent.kind, "already has __create"); } else { kindConstructor.prototype.__create = kindConstructor.prototype.create; kindConstructor.prototype.create = aresOptionCreate; } } } } }, //* When the designer is closed, clean up the last rendered kind cleanUpKind: function() { // Clean up after previous kind if(this.parentInstance) { this.cleanUpPreviousKind(null); this.parentInstance = null; this.selection = null; } }, //* Clean up previously rendered kind cleanUpPreviousKind: function(inKindName) { // Save changes made to components into previously rendered kind's _kindComponents_ array if(this.parentInstance.kind !== inKindName) { enyo.constructorForKind(this.parentInstance.kind).prototype.kindComponents = enyo.json.codify.from(this.$.serializer.serialize(this.parentInstance, true)); } // Reset flags on previously rendered kind's _kindComponents_ array this.unflagAresComponents(); // Clear previously rendered kind this.$.client.destroyClientControls(); // Remove selection and drop highlighting this.hideSelectHighlight(); this.unhighlightDropTargets(); }, resized: function() { this.inherited(arguments); this.highlightSelection(); }, /** Response to message sent from Deimos. Highlight the specified conrol and send a message with a serialized copy of the control. */ selectItem: function(inItem) { if(!inItem) { return; } for(var i=0, c;(c=this.flattenChildren(this.$.client.children)[i]);i++) { if(c.aresId === inItem.aresId) { // this method is typically called right after a // render. The algorithm used in there will query the // DOM to get the boundary and coordinates of the // selected kind. This query must be issued only when // the rendering phase is finished, lest bogus data // are used to draw the highlight rectangle. The call // to timeout ensures that _selectItem is called once // the rendering phase is done. setTimeout(this._selectItem.bind(this, c), 0); return; } } }, //* Update _this.selection_ property value based on change in Inspector modifyProperty: function(inData, inFileName) { if (typeof inData.value === "undefined") { this.removeProperty(inData.property); } else { this.updateProperty(inData.property, inData.value); } this.rerenderKind(inFileName); this.selectItem(this.selection); }, removeProperty: function(inProperty) { delete this.selection[inProperty]; }, updateProperty: function(inProperty, inValue) { var options = this.selection.__aresOptions; if (options && options.isRepeater && (inProperty === "onSetupItem" || inProperty === "count")) { // DO NOT APPLY changes to the properties mentioned above // TODO: could be managed later on thru config in .design files if more than one kind need special processings. this.trace("Skipping modification of \"", inProperty, "\""); } else { this.selection[inProperty] = inValue; } }, //* Get each kind component individually flattenKindComponents: function(inComponents) { var ret = [], cs, c; if(!inComponents) { return ret; } for (var i=0;(c = inComponents[i]);i++) { ret.push(c); if(c.components) { cs = this.flattenKindComponents(c.components); for (var j=0;(c = cs[j]);j++) { ret.push(c); } } } return ret; }, manageComponentsOptions: function(inComponents) { var c; for (var i=0;(c = inComponents[i]);i++) { this.manageComponentOptions(c); if (c.components) { this.manageComponentsOptions(c.components); } } }, manageComponentOptions: function(inComponent) { if (inComponent.__aresOptions) { var options = inComponent.__aresOptions; if (options.isRepeater === true) { /* We are handling a Repeater or a List. Force "count" to 1 and invalidate "onSetupItem" to manage them correctly in the Designer */ this.trace("Manage repeater ", inComponent.kind, inComponent); inComponent.count = 1; inComponent.onSetupItem = "aresUnImplemetedFunction"; } } }, // TODO - merge this with flattenKindComponents() flattenChildren: function(inComponents) { var ret = [], cs, c; for (var i=0;(c = inComponents[i]);i++) { ret.push(c); if(c.children) { cs = this.flattenChildren(c.children); for (var j=0;(c = cs[j]);j++) { ret.push(c); } } } return ret; }, //* Set up drag and drop attributes for component in _inComponents_ makeComponentsDragAndDrop: function(inComponents) { for(var i=0, component;(component = inComponents[i]);i++) { this.makeComponentDragAndDrop(component); } }, //* Set up drag and drop for _inComponent_ makeComponentDragAndDrop: function(inComponent) { this.makeComponentDraggable(inComponent); this.makeComponentADropTarget(inComponent); this.flagAresComponent(inComponent); }, //* Add the attribute _draggable="true"_ to _inComponent_ makeComponentDraggable: function(inComponent) { if(inComponent.attributes) { inComponent.attributes.draggable = true; } else { inComponent.attributes = { draggable: true }; } }, /** Add the attribute _dropTarget="true"_ to _inComponent_ if it wasn't explicitly set to false in the design.js file (works as an opt out). */ makeComponentADropTarget: function(inComponent) { if(inComponent.attributes) { // TODO: Revisit this, once indexer's propertyMetaData is integrated inComponent.attributes.dropTarget = true; //(this.containerData[inComponent.kind] !== false); } else { inComponent.attributes = { dropTarget: (this.containerData[inComponent.kind] !== false) }; } }, flagAresComponent: function(inComponent) { inComponent.aresComponent = true; }, //* Remove _aresComponent_ flag from previously used _this.aresComponents_ array unflagAresComponents: function() { for(var i=0, component;(component = this.aresComponents[i]);i++) { delete component.aresComponent; } }, isValidDropTarget: function(inControl) { return (inControl && inControl !== this.selection && !inControl.isDescendantOf(this.selection)); }, getControlById: function(inId, inContainer) { inContainer = inContainer || this.$.client; for(var i=0, c;(c=this.flattenChildren(inContainer.children)[i]);i++) { if(c.aresId === inId) { return c; } } }, getEventDragTarget: function(inComponent) { return (!inComponent) ? null : (!this.isDraggable(inComponent)) ? this.getEventDragTarget(inComponent.parent) : inComponent; }, getEventDropTarget: function(inComponent) { return (!inComponent) ? null : (inComponent === this.parentInstance) ? this.parentInstance : (!this.isDropTarget(inComponent)) ? this.getEventDropTarget(inComponent.parent) : inComponent; }, isDraggable: function(inComponent) { return (inComponent.attributes && inComponent.attributes.draggable); }, isDropTarget: function(inComponent) { return (inComponent.attributes && inComponent.attributes.dropTarget); }, //* Highlight _inComponent_ with drop target styling, and unhighlight everything else highlightDropTarget: function(inComponent) { this.$.dropHighlight.setShowing(true); this.$.dropHighlight.setBounds(inComponent.hasNode().getBoundingClientRect()); }, unhighlightDropTargets: function() { this.$.dropHighlight.setShowing(false); }, //* Highlight _this.selection_ with selected styling, and unhighlight everything else highlightSelection: function() { this.unhighlightDropTargets(); this.renderSelectHighlight(); }, renderSelectHighlight: function() { if(this.selection && this.selection.hasNode()) { var b = this.selection.hasNode().getBoundingClientRect(); this.$.selectHighlight.setBounds(b); this.$.selectHighlight.show(); if (this.$.selectHighlightCopy) { this.$.selectHighlightCopy.setBounds(b); this.$.selectHighlightCopy.show(); } // Resize handle rendering this.hideAllResizeHandles(); if (this.absolutePositioningMode(this.selection.parent)) { this.showAllResizeHandles(); } else { this.showBottomRightResizeHandle(); } } }, hideSelectHighlight: function() { this.$.selectHighlight.hide(); if (this.$.selectHighlightCopy) { this.$.selectHighlightCopy.hide(); } this.hideAllResizeHandles(); }, syncDropTargetHighlighting: function() { var dropTarget = this.currentDropTarget ? this.$.serializer.serializeComponent(this.currentDropTarget, true) : null; this.sendMessage({op: "syncDropTargetHighlighting", val: dropTarget}); }, //* Set _inItem_ to _this.selected_ and notify Deimos _selectItem: function(inItem, noMessage) { this.selection = inItem; this.highlightSelection(); if (noMessage) { return; } this.sendMessage({op: "select", val: this.$.serializer.serializeComponent(this.selection, true)}); }, /** Find any children in _inControl_ that match kind components of the parent instance, and make them drag/droppable (if appropriate) */ setupControlDragAndDrop: function(inControl) { var childComponents = this.flattenChildren(inControl.children), i, j; this.makeComponentDragAndDrop(inControl); for(i=0;i<childComponents.length;i++) { for(j=0;j<this.aresComponents.length;j++) { if(childComponents[i].aresId === this.aresComponents[j].aresId) { this.makeComponentDragAndDrop(childComponents[i]); } } } }, //* Create object that is a copy of the passed in component getSerializedCopyOfComponent: function(inComponent) { return enyo.json.codify.from(this.$.serializer.serializeComponent(inComponent, true)); }, //* Eval code passed in by designer codeUpdate: function(inCode) { /* jshint evil: true */ eval(inCode); // TODO: ENYO-2074, replace eval. /* jshint evil: false */ }, //* Update CSS by replacing the link/style tag in the head with an updated style tag cssUpdate: function(inData) { if(!inData.filename || !inData.code) { enyo.warn("Invalid data sent for CSS update:", inData); return; } var filename = inData.filename, code = inData.code, head = document.getElementsByTagName("head")[0], links = head.getElementsByTagName("link"), styles = head.getElementsByTagName("style"), el, i ; // Look through link tags for a linked stylesheet with a filename matching _filename_ for(i = 0; (el = links[i]); i++) { if(el.getAttribute("rel") === "stylesheet" && el.getAttribute("type") === "text/css" && el.getAttribute("href") === filename) { this.updateStyle(filename, code, el); return; } } // Look through style tags for a tag with a data-href property matching _filename_ for(i=0;(el = styles[i]);i++) { if(el.getAttribute("data-href") === filename) { this.updateStyle(filename, code, el); return; } } }, //* Replace _inElementToReplace_ with a new style tag containing _inNewCode_ updateStyle: function(inFilename, inNewCode, inElementToReplace) { var head = document.getElementsByTagName("head")[0], newTag = document.createElement("style"); newTag.setAttribute("type", "text/css"); newTag.setAttribute("data-href", inFilename); newTag.innerHTML = inNewCode; head.insertBefore(newTag, inElementToReplace); head.removeChild(inElementToReplace); }, holdOver: function(inEvent) { var container = this.getCurrentDropTarget(); if (!container) { return; } if (this.absolutePositioningMode(container)) { this.absolutePositioningHoldover(inEvent, container); } else { this.staticPositioningHoldover(inEvent, container); } }, absolutePositioningHoldover: function(inEvent, inContainer) { this.setContainerItem(inContainer); this.setBeforeItem(null); this.absolutePrerenderDrop(inEvent); }, absolutePrerenderDrop: function(inEvent) { var x = this.getAbsoluteXPosition(inEvent), y = this.getAbsoluteYPosition(inEvent) ; this.moveSelectionToAbsolutePosition(x, y); }, // Move selection to new position moveSelectionToAbsolutePosition: function(inX, inY) { var container = this.getContainerItem(), clone = this.cloneControl(this.selection, true) //this.createSelectionGhost(this.selection) ; this.hideSelectHighlight(); this.selection.destroy(); this.selection = container.createComponent(clone).render(); this.selection.applyStyle("position", "absolute"); this.selection.applyStyle("pointer-events", "none"); this.addVerticalPositioning(this.selection, inY); this.addHorizontalPositioning(this.selection, inX); }, //* Add appropriate vertical positioning to _inControl_ based on _inY_ addVerticalPositioning: function(inControl, inY) { var container = this.getContainerItem(), containerBounds = this.getRelativeBounds(container), controlBounds = this.getRelativeBounds(inControl), styleProps = {} ; // Convert css string to hash enyo.Control.cssTextToDomStyles(this.trimWhitespace(inControl.style), styleProps); if (styleProps.top || (!styleProps.top && !styleProps.bottom)) { inControl.applyStyle("top", inY + "px"); } if (styleProps.bottom) { inControl.applyStyle("bottom", (containerBounds.height - inY - controlBounds.height) + "px"); } }, //* Add appropriate horizontal positioning to _inControl_ based on _inX_ addHorizontalPositioning: function(inControl, inX) { var container = this.getContainerItem(), containerBounds = this.getRelativeBounds(container), controlBounds = this.getRelativeBounds(inControl), styleProps = {} ; // Convert css string to hash enyo.Control.cssTextToDomStyles(this.trimWhitespace(inControl.style), styleProps); if (styleProps.left || (!styleProps.left && !styleProps.right)) { inControl.applyStyle("left", inX + "px"); } if (styleProps.right) { inControl.applyStyle("right", (containerBounds.width - inX - controlBounds.width) + "px"); } }, trimWhitespace: function(inStr) { inStr = inStr || ""; return inStr.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); }, staticPositioningHoldover: function(inEvent, inContainer) { var x = inEvent.clientX, y = inEvent.clientY, onEdge = this.checkDragOverBoundary(inContainer, x, y), beforeItem; if (onEdge < 0) { beforeItem = inContainer; inContainer = inContainer.parent; } else if (onEdge > 0) { beforeItem = this.findAfterItem(inContainer.parent, inContainer, x, y); inContainer = inContainer.parent; } else { beforeItem = (inContainer.children.length > 0) ? this.findBeforeItem(inContainer, x, y) : null; inContainer = inContainer; } this.setContainerItem(inContainer); this.setBeforeItem(beforeItem); this.staticPrerenderDrop(); }, //* Handle drop that has been trigged from outside of the designerFrame foreignPrerenderDrop: function(inData) { var containerItem = this.getControlById(inData.targetId), beforeItem = inData.beforeId ? this.getControlById(inData.beforeId) : null ; this.setContainerItem(containerItem); this.setBeforeItem(beforeItem); // Do static prerender drop if not an AbsolutePositioningLayout container if (!this.absolutePositioningMode(containerItem)) { this.staticPrerenderDrop(); } }, staticPrerenderDrop: function() { var movedControls, movedInstances; // If not a legal drop, do nothing if (!this.legalDrop()) { return; } // Create copy of app in post-move state this.renderUpdatedAppClone(); // Figure which controls need to move to get to the new state movedControls = this.getMovedControls(); // Hide app copy this.hideUpdatedAppClone(); // Move (hidden) selected control to new position this.moveSelection(); // Hide any controls that need to be moved movedInstances = this.hideMovedControls(movedControls); // Turn off selection/drop area highlighting this.hideSelectHighlight(); this.unhighlightDropTargets(); // Create copies of controls that need to move, and animate them to the new posiitions this.animateMovedControls(movedControls); // When animation completes, udpate parent instance to reflect changes. TODO - don't use setTimeout, do this on an event when the animation completes setTimeout(enyo.bind(this, function() { this.prerenderMoveComplete(movedInstances); }), this.moveControlSecs*1000 + 100); }, prerenderMoveComplete: function(inInstances) { // Hide layer with flying controls this.$.flightArea.applyStyle("display", "none"); // Show hidden controls in app this.showMovedControls(inInstances); // Point _this.parentInstance_ to current client controls this.parentInstance = this.$.client.getClientControls()[0]; }, legalDrop: function() { var containerId = (this.getContainerItem()) ? this.getContainerItem().aresId : null, beforeId = (this.getBeforeItem()) ? this.getBeforeItem().aresId : null; // If creating a new item, drop is legal if (this.getCreatePaletteItem()) { return true; } if ((!this.getContainerItem() || !this.selection) || this.selection.aresId === containerId || this.selection.aresId === beforeId) { return false; } return true; }, //* Return all controls that will be affected by this move getMovedControls: function() { var originalPositions = this.getControlPositions(this.$.client.children), updatedPositions = this.getControlPositions(this.$.cloneArea.children), movedControls = [], originalItem, updatedItem, i, j; for (i = 0; (originalItem = originalPositions[i]); i++) { for (j = 0; (updatedItem = updatedPositions[j]); j++) { if (originalItem.comp.aresId === updatedItem.comp.aresId && !this.rectsAreEqual(originalItem.rect, updatedItem.rect)) { movedControls.push({ comp: originalItem.comp, origStyle: this.cloneCSS(enyo.dom.getComputedStyle(originalItem.comp.hasNode())), newStyle: this.cloneCSS(enyo.dom.getComputedStyle(updatedItem.comp.hasNode())), origRect: originalItem.rect, newRect: updatedItem.rect }); } } } return movedControls; }, cloneCSS: function(inCSS) { for(var i = 0, cssText = ""; i < inCSS.length; i++) { if ( inCSS[i] === "position" || inCSS[i] === "top" || inCSS[i] === "left" || inCSS[i] === "-webkit-transition-duration" || inCSS[i] === "-webkit-transition-property" ) { continue; } cssText += inCSS[i] + ": " + inCSS.getPropertyValue(inCSS[i]) + "; "; } return cssText; }, hideMovedControls: function(inControls) { var originalControls = this.flattenChildren(this.$.client.children), hiddenControls = []; for (var i = 0; i < originalControls.length; i++) { if (!originalControls[i].aresId) { continue; } for (var j = 0; j < inControls.length; j++) { if (inControls[j].comp.aresId && originalControls[i].aresId === inControls[j].comp.aresId) { originalControls[i].applyStyle("opacity", "0"); hiddenControls.push(originalControls[i]); } } } return hiddenControls; }, // Move selection to new position moveSelection: function() { var containerId = (this.getContainerItem()) ? this.getContainerItem().aresId : null, container = this.getControlById(containerId), beforeId = (this.getBeforeItem()) ? this.getBeforeItem().aresId : null, before = (beforeId) ? this.getControlById(beforeId) : null, clone = this.cloneControl(this.selection, true); //this.createSelectionGhost(this.selection); // If the selection should be moved before another item, use the _addBefore_ property if (before) { clone = enyo.mixin(clone, {beforeId: beforeId, addBefore: before}); } // If the selection has absolute positioning applied, remove it if (clone.style) { clone.style = this.removeAbsolutePositioningStyle(clone); } this.selection.destroy(); this.selection = container.createComponent(clone).render(); }, removeAbsolutePositioningStyle: function(inControl) { var currentStyle = inControl.style || "", styleProps = currentStyle.split(";"), updatedProps = [], prop, i; for (i = 0; i < styleProps.length; i++) { prop = styleProps[i].split(":"); if (prop[0].match(/position/) || prop[0].match(/top/) || prop[0].match(/left/)) { continue; } updatedProps.push(styleProps[i]); } for (i = 0, currentStyle = ""; i < updatedProps.length; i++) { currentStyle += updatedProps[i]; } return currentStyle; }, //* Draw controls that will do aniumating at starting points and then kick off animation animateMovedControls: function(inControls) { this.renderAnimatedControls(inControls); this.animateAnimatedControls(); }, renderAnimatedControls: function(inControls) { // Clean up existing animated controls this.$.flightArea.destroyClientControls(); // Create a copy of each control that is being moved for (var i = 0, control; i < inControls.length; i++) { if (inControls[i].comp.aresId === this.selection.aresId) { control = this.$.flightArea.createComponent( { kind: "enyo.Control", aresId: inControls[i].comp.aresId, moveTo: inControls[i].newRect, newStyle: inControls[i].newStyle } ); control.addStyles("z-index:1000;"); } else { control = this.$.flightArea.createComponent( { kind: inControls[i].comp.kind, content: inControls[i].comp.getContent(), aresId: inControls[i].comp.aresId, moveTo: inControls[i].newRect, newStyle: inControls[i].newStyle } ); } // Set the starting top/left values and props to enable animation control.addStyles( inControls[i].origStyle + "position: absolute; " + "top: " + inControls[i].origRect.top + "px; " + "left: " + inControls[i].origRect.left + "px; " + "-webkit-transition-duration: " + this.moveControlSecs + "s; " + "-webkit-transition-property: all; " ); } // Render animated controls this.$.flightArea.render().applyStyle("display", "block"); }, animateAnimatedControls: function() { var controls = this.$.flightArea.getClientControls(); setTimeout(function() { for(var i=0;i<controls.length;i++) { controls[i].addStyles( controls[i].newStyle + "position: absolute; " + "top: " + controls[i].moveTo.top + "px; " + "left: " + controls[i].moveTo.left + "px; " ); controls[i].render(); } }, 0); }, //* Show controls that were hidden for the move showMovedControls: function(inControls) { for (var i = 0; i < inControls.length; i++) { inControls[i].applyStyle("opacity", "1"); } }, //* Render updated copy of the parentInstance into _cloneArea_ renderUpdatedAppClone: function() { this.$.cloneArea.destroyClientControls(); this.$.cloneArea.applyStyle("display", "block"); var appClone = this.$.cloneArea.createComponent(this.cloneControl(this.parentInstance)); // Mimic top-level app fitting (as if this was rendered with renderInto or write) if (this.parentInstance.fit) { appClone.addClass("enyo-fit enyo-clip"); } var containerId = (this.getContainerItem()) ? this.getContainerItem().aresId : null, container = this.getControlById(containerId, this.$.cloneArea), beforeId = (this.getBeforeItem()) ? this.getBeforeItem().aresId : null, before = (beforeId) ? this.getControlById(beforeId, this.$.cloneArea) : null, selection = this.getControlById(this.selection.aresId, this.$.cloneArea), clone = this.cloneControl(this.selection, true) ; if (before) { clone = enyo.mixin(clone, {beforeId: beforeId, addBefore: before}); } // If the selection has absolute positioning applied, remove it if (clone.style) { clone.style = this.removeAbsolutePositioningStyle(clone); } container.createComponent(clone); if (selection) { selection.destroy(); } this.$.cloneArea.render(); }, hideUpdatedAppClone: function() { this.$.cloneArea.destroyClientControls(); this.$.cloneArea.applyStyle("display", "none"); }, //* TODO - This createSelectionGhost is WIP createSelectionGhost: function (inItem) { var computedStyle = enyo.dom.getComputedStyle(inItem.hasNode()), borderWidth = 1, style; if (!computedStyle) { enyo.warn("Attempted to clone item with no node: ", inItem); return null; } this.log("h: ", parseInt(computedStyle.height, 10), "w: ", parseInt(computedStyle.width, 10), "p: ", parseInt(computedStyle.padding, 10), "m: ", parseInt(computedStyle.margin,10)); style = "width: " + computedStyle.width + "; " + "height: " + computedStyle.height + "; " + //"margin: " + computedStyle.margin + "; " + //"padding: " + computedStyle.padding + "; " + "border: " + borderWidth + "px dotted black; " + "display: " + computedStyle.display + "; " + "background: rgba(255,255,255,0.8); "; return { kind: "enyo.Control", aresId: inItem.aresId, style: style }; }, cloneControl: function(inSelection, inCloneChildren) { var clone = { layoutKind: inSelection.layoutKind, content: inSelection.content, aresId: inSelection.aresId, classes: inSelection.classes, style: inSelection.style }; // if inSelection.kind is undefined, let enyo apply the defaultKind if (inSelection.kind) { clone.kind = inSelection.kind; } if (inCloneChildren) { clone.components = this.cloneChildComponents(inSelection.components); } return clone; }, cloneChildComponents: function(inComponents) { var childComponents = []; if (!inComponents || inComponents.length === 0) { return childComponents; } for (var i = 0, comp; (comp = inComponents[i]); i++) { childComponents.push(this.cloneControl(comp, true)); } return childComponents; }, getControlPositions: function(inComponents) { var controls = this.flattenChildren(inComponents), positions = []; for(var i=0;i<controls.length;i++) { if (controls[i].aresId) { positions.push({comp: controls[i], rect: controls[i].hasNode().getBoundingClientRect()}); } } return positions; }, rectsAreEqual: function(inRectA, inRectB) { return (inRectA.top === inRectB.top && inRectA.left === inRectB.left && inRectA.bottom === inRectB.bottom && inRectA.right === inRectB.right && inRectA.height === inRectB.height && inRectA.width === inRectB.width); }, checkDragOverBoundary: function(inContainer, x, y) { if (!inContainer) { return 0; } var bounds = inContainer.hasNode().getBoundingClientRect(); if (x - bounds.left <= this.edgeThresholdPx) { return -1; } else if ((bounds.left + bounds.width) - x <= this.edgeThresholdPx) { return 1; } else if (y - bounds.top <= this.edgeThresholdPx) { return -1; } else if ((bounds.top + bounds.height) - y <= this.edgeThresholdPx) { return 1; } else { return 0; } }, findBeforeItem: function(inContainer, inX, inY) { if (!inContainer) { return null; } var childData = [], aboveItems, belowItems, rightItems, sameRowItems; // Build up array of nodes for (var i = 0; i < inContainer.children.length; i++) { if (inContainer.children[i].hasNode()) { childData.push(enyo.mixin( enyo.clone(inContainer.children[i].node.getBoundingClientRect()), {aresId: inContainer.children[i].aresId} )); } } aboveItems = this.findAboveItems(childData, inY); // If no above items, place as the first item in this container if (aboveItems.length === 0) { return childData[0]; } belowItems = this.findBelowItems(childData, inY); // If no below items, place as the last item in this container if (belowItems.length === 0) { return null; } // Items on the same row are both above and below the dragged item sameRowItems = this.removeDuplicateItems(aboveItems, belowItems); // If we have items on the same row as the dragged item, find the first item to the left if (sameRowItems.length > 0) { // If on the same row but no left items, place as the first item on this row if (this.findLeftItems(sameRowItems, inX).length === 0) { return this.filterArrayForMinValue(sameRowItems, "left"); // If there are left items, the leftmost right item becomes the before item } else { rightItems = this.findRightItems(sameRowItems, inX); // If there are no items to the right, insert before topmost and leftmost below item if(rightItems.length === 0) { return this.filterArrayForMinValue(this.findLeftmostItems(belowItems), "top", inY); // If there are items to the right, return the leftmost one } else { return this.filterArrayForMinValue(rightItems, "left"); } } } // If there are no items on the same row as this one, insert before topmost and leftmost below item return this.filterArrayForMinValue(this.findLeftmostItems(belowItems), "top"); }, //* Return the item in _inContaienr_ that is immediately "after" _inItem_ findAfterItem: function(inContainer, inItem, inX, inY) { if (!inContainer) { return null; } var childData = [], aboveItems, belowItems, sameRowItems; for (var i = 0; i < inContainer.children.length; i++) { if (inContainer.children[i].hasNode()) { childData.push(enyo.mixin( enyo.clone(inContainer.children[i].node.getBoundingClientRect()), {aresId: inContainer.children[i].aresId} )); } } // Filter out _inItem_ from _aboveItems_ aboveItems = this.findAboveItems(childData, inY).filter(function(elem, pos, self) { return elem.aresId !== inItem.aresId; }); // Filter out _inItem_ from _belowItems_ belowItems = this.findBelowItems(childData, inY).filter(function(elem, pos, self) { return elem.aresId !== inItem.aresId; }); // If no below items, place as the last item in this container if (belowItems.length