/* This file contains proprietary software owned by Motorola Mobility, Inc.
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved.
*/ var Montage = require("montage/core/core").Montage; var Component = require("montage/ui/component").Component; var nj = require("js/lib/NJUtils").NJUtils; var TimelinePanel = exports.TimelinePanel = Montage.create(Component, { hasTemplate:{ value:true }, /* === BEGIN: Models === */ _arrLayers:{ value:[] }, arrLayers:{ serializable:true, get:function () { return this._arrLayers; }, set:function (newVal) { this._arrLayers = newVal; this.needsDraw = true; this.cacheTimeline(); } }, _temparrLayers:{ value:[] }, temparrLayers:{ get:function () { return this._temparrLayers; }, set:function (newVal) { this._temparrLayers = newVal; } }, _layerRepetition:{ value:null }, layerRepetition:{ get:function () { return this._layerRepetition; }, set:function (newVal) { this._layerRepetition = newVal; } }, _areTracksScrolling: { value: false }, // Set to false to skip array caching array sets in currentDocument _boolCacheArrays:{ value:true }, // Current layer number: iterated and used to assign layer IDs. _currentLayerNumber:{ value:0 }, currentLayerNumber:{ get:function () { return this._currentLayerNumber; }, set:function (newVal) { if (newVal !== this._currentLayerNumber) { this._currentLayerNumber = newVal; this.cacheTimeline(); } } }, _currentLayerSelected:{ value: false }, currentLayerSelected:{ get:function () { return this._currentLayerSelected; }, set:function (newVal) { this._currentLayerSelected = newVal; this.cacheTimeline(); } }, _currentElementsSelected: { value: [] }, currentElementsSelected: { get: function() { return this._currentElementsSelected; }, set: function(newVal) { this._currentElementsSelected = newVal; this.cacheTimeline(); } }, _currentLayersSelected:{ value:[] }, currentLayersSelected:{ get:function () { return this._currentLayersSelected; }, set:function (newVal) { this._currentLayersSelected = newVal; this.cacheTimeline(); } }, // The index of the last layer that was clicked on // (used for shift-click multiselect) _lastLayerClicked : { value: 0 }, lastLayerClicked: { serializable: true, get: function() { return this._lastLayerClicked; }, set: function(newVal) { this._lastLayerClicked = newVal } }, _currentSelectedContainer: { value: null }, currentSelectedContainer: { get: function() { return this._currentSelectedContainer; }, set: function(newVal) { this._currentSelectedContainer = newVal; this.handleDocumentChange(); } }, _millisecondsOffset:{ value:1000 }, millisecondsOffset:{ get:function () { return this._millisecondsOffset; }, set:function (newVal) { if (newVal !== this._millisecondsOffset) { this._millisecondsOffset= newVal; this.drawTimeMarkers(); NJevent('tlZoomSlider',this); } } }, _masterDuration:{ value:0 }, masterDuration:{ serializable:true, get:function () { return this._masterDuration; }, set:function (val) { this._masterDuration = val; var intDur = Math.round(val/12), strWidth = intDur + "px"; this.timebar.style.width = strWidth; } }, _trackRepetition:{ value:null }, trackRepetition:{ get:function () { return this._trackRepetition; }, set:function (newVal) { this._trackRepetition = newVal; } }, _selectedKeyframes:{ value:[] }, selectedKeyframes:{ serializable:true, get:function () { return this._selectedKeyframes; }, set:function (newVal) { this._selectedKeyframes = newVal; } }, _selectedTweens:{ value:[] }, selectedTweens:{ serializable:true, get:function () { return this._selectedTweens; }, set:function (newVal) { this._selectedTweens = newVal; } }, _breadCrumbContainer:{ value:null }, breadCrumbContainer:{ get:function () { return this._breadCrumbContainer; }, set:function (value) { if (this._breadCrumbContainer !== value) { this._breadCrumbContainer = value; } } }, _firstTimeLoaded:{ value:true }, _captureSelection:{ value:false }, _openDoc:{ value:false }, timeMarkerHolder:{ value:null }, // Drag and Drop properties _dragAndDropHelper : { value: false }, _dragAndDropHelperCoords: { value: false }, _dragAndDropHelperOffset : { value: false }, _dragLayerID : { value: null }, _draggingType: { value: false }, draggingType: { get: function() { return this._draggingType; }, set: function(newVal) { this._draggingType = newVal; } }, layersDragged:{ value:[], writable:true }, dragLayerID : { get: function() { return this._dragLayerID; }, set: function(newVal) { if (newVal !== this._dragLayerID) { this._dragLayerID = newVal; } } }, _dragLayerIndexes: { value: [] }, _dropLayerID : { value: null }, dropLayerID : { get: function() { return this._dropLayerID; }, set: function(newVal) { if (newVal !== this._dropLayerID) { this._dropLayerID = newVal; var dropLayerIndex = this.getLayerIndexByID(this.dropLayerID), arrDragLayers = [], i = 0, dragLayerIndexesLength = this._dragLayerIndexes.length; // TODO: possibly we'll need to sort dragLayerIndexes so things don't get out of order? for (i = 0; i < dragLayerIndexesLength; i++) { var myDraggingLayer = this.arrLayers[this._dragLayerIndexes[i]]; arrDragLayers.push(myDraggingLayer); // Splice arrLayers this.arrLayers.splice(this._dragLayerIndexes[i], 1); this.arrLayers.splice(dropLayerIndex, 0, myDraggingLayer); } this.layersDragged = arrDragLayers; this._layerDroppedInPlace = this.arrLayers[dropLayerIndex]; // Cache the new info this.cacheTimeline(); // Clear drag and drop variables for future re-use this._dropLayerID = null; this.dragLayerIndexes = []; this._dragLayerIndexes = []; this.lastLayerClicked = 0; // Sometimes, just to be fun, the drop and dragend events don't fire. // So just in case, set the draw routine to delete the helper. this._deleteHelper = true; this.needsDraw = true; } } }, _appendHelper: { value: false }, _deleteHelper: { value: false }, _scrollTracks: { value: false }, // Keyframe drag and drop properties _draggingTrackId: { value: false }, draggingTrackId: { get: function() { return this._draggingTrackId; }, set: function(newVal) { this._draggingTrackId = newVal; } }, useAbsolutePosition:{ value:true }, _currentDocumentUuid: { value: false }, _ignoreSelectionChanges: { value: false }, // is the control key currently being pressed (used for multiselect) _isControlPressed: { value: false }, // is the shift key currently being pressed (used for multiselect) _isShiftPressed: { value: false }, /* === END: Models === */ /* === BEGIN: Draw cycle === */ prepareForDraw:{ value:function () { this.initTimeline(); } }, draw:{ value: function() { // Drag and Drop: if (this.draggingType === "layer") { // Do we have a helper to append? if (this._appendHelper === true) { this.container_layers.appendChild(this._dragAndDropHelper); this._appendHelper = false; } // Do we need to move the helper? if (this._dragAndDropHelperCoords !== false) { if (this._dragAndDropHelper !== null) { this._dragAndDropHelper.style.top = this._dragAndDropHelperCoords; } this._dragAndDropHelperCoords = false; } // Do we need to scroll the tracks? if (this._scrollTracks !== false) { this.layout_tracks.scrollTop = this._scrollTracks; this._scrollTracks = false; } // Do we have a helper to delete? if (this._deleteHelper === true) { if (this._dragAndDropHelper === null) { // Problem....maybe a helper didn't get appended, or maybe it didn't get stored. // Try and recover the helper so we can delete it. var myHelper = this.container_layers.querySelector(".timeline-dnd-helper"); if (myHelper != null) { this._dragAndDropHelper = myHelper; } } if (this._dragAndDropHelper !== null) { // We need to delete the helper. Can we delete it from container_layers? if (this._dragAndDropHelper && this._dragAndDropHelper.parentNode === this.container_layers) { this.container_layers.removeChild(this._dragAndDropHelper); this._dragAndDropHelper = null; this._deleteHelper = false; } } this.application.ninja.elementMediator.reArrangeDOM(this.layersDragged , this._layerDroppedInPlace); this.layersDragged =[]; } } else if (this.draggingType === "keyframe") { // Do we need to scroll the tracks? if (this._scrollTracks !== false) { this.layout_tracks.scrollLeft = this._scrollTracks; this._scrollTracks = false; } } // Do we need to scroll the layers? if (this._areTracksScrolling) { this._areTracksScrolling = false; this.user_layers.scrollTop = this.layout_tracks.scrollTop; this.layout_markers.scrollLeft = this.layout_tracks.scrollLeft; this.playheadmarker.style.top = this.layout_tracks.scrollTop + "px"; } } }, /* === END: Draw cycle === */ /* === BEGIN: Controllers === */ // Create an empty layer template object with minimal defaults and return it for use createLayerTemplate:{ value:function () { var returnObj = {}; returnObj.layerData = {}; returnObj.layerData.layerName = null; returnObj.layerData.layerID = null; returnObj.layerData.stageElement = null; returnObj.layerData.isMainCollapsed = true; returnObj.layerData.isPositionCollapsed = true; returnObj.layerData.isTransformCollapsed = true; returnObj.layerData.isStyleCollapsed = true; returnObj.layerData.arrLayerStyles = []; returnObj.layerData.elementsList = []; returnObj.layerData.deleted = false; returnObj.layerData.isSelected = false; returnObj.layerData.layerPosition = null; returnObj.layerData.created = false; returnObj.layerData.isTrackAnimated = false; returnObj.layerData.currentKeyframeRule = null; returnObj.layerData.trackPosition = 0; returnObj.layerData.arrStyleTracks = []; returnObj.layerData.tweens = []; returnObj.layerData.layerTag = ""; returnObj.layerData.isVisible = true; returnObj.layerData.docUUID = this.application.ninja.currentDocument._uuid; returnObj.layerData.isTrackAnimated = false; returnObj.layerData.triggerBinding = false; returnObj.parentElementUUID = null; returnObj.parentElement = null; return returnObj; } }, // cache Timeline data in currentDocument. cacheTimeline: { value: function() { // Store the timeline data in currentDocument... if (this._boolCacheArrays) { // ... but only if we're supposed to. this.application.ninja.currentDocument.tlArrLayers = this.arrLayers; this.application.ninja.currentDocument.tlCurrentSelectedContainer = this.application.ninja.currentSelectedContainer; this.application.ninja.currentDocument.tllayerNumber = this.currentLayerNumber; this.application.ninja.currentDocument.tlCurrentLayerSelected = this.currentLayerSelected; this.application.ninja.currentDocument.tlCurrentLayersSelected = this.currentLayersSelected; this.application.ninja.currentDocument.tlCurrentElementsSelected = this.currentElementsSelected; } } }, // Initialize Timeline cache in currentDocument. initTimelineCache: { value: function() { // Initialize the currentDocument for a new set of timeline data. this.application.ninja.currentDocument.isTimelineInitialized = true; this.application.ninja.currentDocument.tlArrLayers = []; this.application.ninja.currentDocument.tlCurrentSelectedContainer = this.application.ninja.currentSelectedContainer; this.application.ninja.currentDocument.tllayerNumber = this.currentLayerNumber; this.application.ninja.currentDocument.tlCurrentLayerSelected = false; this.application.ninja.currentDocument.tlCurrentLayersSelected = false; this.application.ninja.currentDocument.tlCurrentElementsSelected = []; } }, // Create an array of style objects for an element, for use // in creating a new layer createLayerStyles : { value: function(ptrElement) { // TODO: Create logic to loop through // CSS properties on element and build // array of layer styles for return. // Right now this method just returns an array of one bogus style. var returnArray = [], newStyle = {}, styleID = "1@0"; // format: layerID + "@" + style counter /* Example new style newStyle.styleID = styleID; newStyle.whichView = "propval"; // Which view do we want to show, usually property/value view (see Style) newStyle.editorProperty = "top"; // the style property newStyle.editorValue = 0; // The current value newStyle.ruleTweener = false; newStyle.isSelected = false; returnArray.push(newStyle); */ return returnArray; } }, // Create an array of style track objects for an element, for use // in creating a new layer createStyleTracks : { value: function(ptrElement) { // TODO: Create logic to loop through // CSS properties on element and build // array of layer styles for return. // Right now this method just returns an array of one bogus style. var returnArray = []; return returnArray; } }, // Bind all document-specific events (pass in true to unbind) _bindDocumentEvents : { value: function(boolUnbind) { /* var arrEvents = [ "stageElement", "deleteLayer", "elementAdded", "elementsRemoved", "elementReplaced", "selectionChange"], */ var arrEvents = ["elementAdded", "elementsRemoved", "selectionChange"], i, arrEventsLength = arrEvents.length; if (boolUnbind) { for (i = 0; i < arrEventsLength; i++) { this.eventManager.removeEventListener(arrEvents[i], this, false); } } else { for (i = 0; i < arrEventsLength; i++) { this.eventManager.addEventListener(arrEvents[i], this, false); } } } }, // Initialize the timeline, runs only once when the timeline component is first loaded initTimeline:{ value:function () { // Get some selectors this.layout_tracks = this.element.querySelector(".layout-tracks"); this.layout_markers = this.element.querySelector(".layout_markers"); // Bind the event handler for the document change events //this.eventManager.addEventListener("onOpenDocument", this.handleDocumentChange.bind(this), false); this.eventManager.addEventListener("closeDocument", this.handleDocumentChange.bind(this), false); //this.eventManager.addEventListener("switchDocument", this.handleDocumentChange.bind(this), false); //this.eventManager.addEventListener("breadCrumbBinding",this,false); // Bind drag and drop event handlers this.container_layers.addEventListener("dragstart", this.handleLayerDragStart.bind(this), false); this.container_layers.addEventListener("dragend", this.handleLayerDragEnd.bind(this), false); this.container_layers.addEventListener("dragover", this.handleLayerDragover.bind(this), false); this.container_layers.addEventListener("drop", this.handleLayerDrop.bind(this), false); this.container_tracks.addEventListener("dragover", this.handleKeyframeDragover.bind(this), false); this.container_tracks.addEventListener("drop", this.handleKeyframeDrop.bind(this), false); // Bind the handlers for the config menu this.checkable_animated.addEventListener("click", this.handleAnimatedClick.bind(this), false); this.checkable_relative.addEventListener("click", this.handleRelativeClick.bind(this), false); this.checkable_absolute.addEventListener("click", this.handleAbsoluteClick.bind(this), false); this.tl_configbutton.addEventListener("click", this.handleConfigButtonClick.bind(this), false); document.addEventListener("click", this.handleDocumentClick.bind(this), false); // Add some event handlers this.timeline_leftpane.addEventListener("click", this.timelineLeftPanelMousedown.bind(this), false); //this.timeline_leftpane.addEventListener("click", this.timelineLeftPanelMousedown.bind(this), false); //this.timeline_leftpane.addEventListener("mouseup", this.timelineLeftPaneMouseup.bind(this), false); this.layout_tracks.addEventListener("scroll", this.updateLayerScroll.bind(this), false); this.user_layers.addEventListener("scroll", this.updateLayerScroll.bind(this), false); this.end_hottext.addEventListener("changing", this.updateTrackContainerWidth.bind(this), false); this.playhead.addEventListener("mousedown", this.startPlayheadTracking.bind(this), false); this.playhead.addEventListener("mouseup", this.stopPlayheadTracking.bind(this), false); this.time_markers.addEventListener("click", this.updatePlayhead.bind(this), false); document.addEventListener("keydown", this.timelineLeftPaneKeydown.bind(this), false); document.addEventListener("keyup", this.timelineLeftPaneKeyup.bind(this), false); this.eventManager.addEventListener("updatedID", this.handleLayerIdUpdate.bind(this), false); // Bind some bindings Object.defineBinding(this, "currentSelectedContainer", { boundObject:this.application.ninja, boundObjectPropertyPath:"currentSelectedContainer", oneway:true }); // Start the panel out in disabled mode by default // (Will be switched on later, if appropriate). this.enablePanel(false); } }, // Initialize the timeline for a document. // Called when a document is opened (new or existing), or when documents are switched. initTimelineForDocument:{ value:function () { var myIndex, boolAlreadyInitialized = false; this.drawTimeMarkers(); // Document switching // Check to see if we have saved timeline information in the currentDocument. //console.log("TimelinePanel.initTimelineForDocument"); if ((typeof(this.application.ninja.currentDocument.isTimelineInitialized) === "undefined")) { //console.log('TimelinePanel.initTimelineForDocument: new Document'); // No, we have no information stored. // This could mean we are creating a new file, OR are opening an existing file. // First, initialize the caches. this.initTimelineCache(); this.temparrLayers = []; // That's all we need to do for a brand new file. // But what if we're opening an existing document? if (!this.application.ninja.documentController.creatingNewFile && this.application.ninja.currentDocument.currentView !== "code") { // Opening an existing document. If it has DOM elements we need to restore their timeline info if (this.application.ninja.currentDocument.model.documentRoot.children[0]) { // Yes, it has DOM elements. Loop through them and create a new object for each. for (myIndex = 0; this.application.ninja.currentDocument.model.documentRoot.children[myIndex]; myIndex++) { this._openDoc = true; this.restoreLayer(this.application.ninja.currentDocument.model.documentRoot.children[myIndex]); } } } // Draw the repetition. this.arrLayers = this.temparrLayers; this.currentLayerNumber = this.arrLayers.length; this._currentDocumentUuid = this.application.ninja.currentDocument.uuid; boolAlreadyInitialized = true; } else if (this.application.ninja.currentDocument.setLevel) { //console.log('TimelinePanel.initTimelineForDocument: breadCrumbClick'); // Information stored, but we're moving up or down in the breadcrumb. // Get the current selection and restore timeline info for its children. //debugger; var parentNode = this.application.ninja.currentSelectedContainer, storedCurrentLayerNumber = this.application.ninja.currentDocument.tllayerNumber; this.temparrLayers = []; for (myIndex = 0; parentNode.children[myIndex]; myIndex++) { this._openDoc = true; this.restoreLayer(parentNode.children[myIndex]); } // Draw the repetition. this.arrLayers = this.temparrLayers; this.currentLayerNumber = storedCurrentLayerNumber; boolAlreadyInitialized = true; this.application.ninja.currentDocument.setLevel = false; } else { //console.log('TimelinePanel.initTimelineForDocument: else fallback'); // we do have information stored. Use it. var i = 0, tlArrLayersLength = this.application.ninja.currentDocument.tlArrLayers.length; // We're reading from the cache, not writing to it. this._boolCacheArrays = false; for (i = 0; i < tlArrLayersLength; i++) { if (this.application.ninja.currentDocument.tlArrLayers[i].layerData.isSelected === true) { this.application.ninja.currentDocument.tlArrLayers[i].layerData._isFirstDraw = true; } else { this.application.ninja.currentDocument.tlArrLayers[i].layerData._isFirstDraw = false; } } this.arrLayers = this.application.ninja.currentDocument.tlArrLayers; this.currentLayerNumber = this.application.ninja.currentDocument.tllayerNumber; this.currentLayerSelected = this.application.ninja.currentDocument.tlCurrentLayerSelected; this.currentLayersSelected = this.application.ninja.currentDocument.tlCurrentLayersSelected; this.currentElementsSelected = this.application.ninja.currentDocument.tlCurrentElementsSelected; this._currentDocumentUuid = this.application.ninja.currentDocument.uuid; // Are we only showing animated layers? if (this.application.ninja.currentDocument.boolShowOnlyAnimated) { // Fake a click. var evt = document.createEvent("MouseEvents"); evt.initMouseEvent("click"); this.checkable_animated.dispatchEvent(evt); } // Ok, done reading from the cache. this._boolCacheArrays = true; // Reset master duration this.resetMasterDuration(); } } }, // Clear the currently-displayed document (and its events) from the timeline. clearTimelinePanel:{ value:function () { // Remove events this._bindDocumentEvents(true); // Remove every event listener for every selected tween in the timeline this.deselectTweens(); // Reset visual appearance // TODO: Maybe playhead position should be stored per document, so we can persist between document switch? this.application.ninja.timeline.playhead.style.left = "-2px"; this.application.ninja.timeline.playheadmarker.style.left = "0px"; this.application.ninja.timeline.updateTimeText(0.00); this.timebar.style.width = "0px"; this.checkable_animated.classList.remove("checked"); this.currentLayerNumber = 0; this.currentLayerSelected = false; this.currentLayersSelected = false; this.currentElementsSelected = []; this.selectedKeyframes = []; this.selectedTweens = []; this._captureSelection = false; this._openDoc = false; this.end_hottext.value = 25; this.updateTrackContainerWidth(); // Clear the repetitions if (this.arrLayers.length > 0) { this.arrLayers = []; this.arrLayers.length = 0; } this.resetMasterDuration(); } }, handleDocumentChange:{ value:function () { // console.log("TimelinePanel.handleDocumentChange"); if (this.application.ninja.currentDocument == null) { // On app initialization, the binding is triggered before // there is a currentDocument. We don't do anything at that time. return; } // Is this the same document? if (this._currentDocumentUuid === this.application.ninja.currentDocument.uuid) { // Yes, same document, so we are changing levels. this.application.ninja.currentDocument.setLevel = true; this._ignoreSelectionChanges = true; } this._boolCacheArrays = false; this.clearTimelinePanel(); this._boolCacheArrays = true; // Rebind the document events for the new document context this._bindDocumentEvents(); // Reinitialize the timeline...but only if there are open documents. if (this.application.ninja.documentController._documents.length > 0) { this.enablePanel(true); this.initTimelineForDocument(); } else { this.enablePanel(false); } } }, updateTrackContainerWidth:{ value:function () { this.container_tracks.style.width = (this.end_hottext.value * 80) + "px"; this.master_track.style.width = (this.end_hottext.value * 80) + "px"; this.time_markers.style.width = (this.end_hottext.value * 80) + "px"; if (this.timeMarkerHolder) { this.time_markers.removeChild(this.timeMarkerHolder); } this.drawTimeMarkers(); } }, updateLayerScroll:{ value:function () { this._areTracksScrolling = true; this.needsDraw = true; } }, startPlayheadTracking:{ value:function () { this.time_markers.onmousemove = this.updatePlayhead.bind(this); } }, stopPlayheadTracking:{ value:function () { this.time_markers.onmousemove = null; } }, updatePlayhead:{ value:function (event) { var clickedPosition = event.target.offsetLeft + event.offsetX; this.playhead.style.left = (clickedPosition - 2) + "px"; this.playheadmarker.style.left = clickedPosition + "px"; var currentMillisecPerPixel = Math.floor(this.millisecondsOffset / 80); var currentMillisec = currentMillisecPerPixel * clickedPosition; this.updateTimeText(currentMillisec); } }, // Event handler for changes in stage selection. handleSelectionChange: { value:function (event) { this.updateLayerSelection(); } }, // Select the layers whose indexes are passed in as arrSelectedIndexes. // Pass in an empty array to clear all selections. selectLayers:{ value:function (arrSelectedIndexes) { var i = 0, j = 0, arrLayersLength = this.arrLayers.length, arrSelectedIndexesLength = arrSelectedIndexes.length, currentLayersSelectedLength = this.currentLayersSelected.length, boolContinue = false, arrSelectedLayers = false, arrCurrentElementsSelected = []; /* // TODO: this should probably check to see if it actually needs to run. console.log(arrSelectedIndexes); console.log(this.currentLayersSelected); // Compare arrSelectedIndexes with this.currentLayersSelected // If the items are the same, we do not need to do anything. if (arrSelectedIndexesLength !== currentLayersSelectedLength) { // Different length in the arrays, we definitely need to continue. console.log('diferent length') boolContinue = true; } else { // Check each selected index and see if it's in this.currentLayersSelected // If we find one that isn't, we need to continue for (i = 0; i < arrSelectedIndexesLength; i++) { console.log('checking for ', arrSelectedIndexes[i]); if (this.currentLayersSelected.indexOf(arrSelectedIndexes[i]) === -1) { // Ooops, one of them was not found. boolContinue = true; } } } if (boolContinue === false) { console.log('exiting') return; } */ // Deselect selected layers if they're not in arrSelectedIndexes. for (i = 0; i < arrLayersLength; i++) { if (this.arrLayers[i].layerData.isSelected === true) { if (arrSelectedIndexes.indexOf(i) < 0) { console.log("TimelinePanel.selectLayers, deselecting ", i); this.arrLayers[i].layerData.isSelected = false; this.triggerLayerBinding(i); } } } if (this.currentLayersSelected !== false) { this.currentLayersSelected = false; } // If we are actually going to be selecting things, create an empty array to use if (arrSelectedIndexesLength > 0) { arrSelectedLayers = []; } // Loop through arrLayers and do the selection. for (i = 0; i < arrLayersLength; i++) { if (arrSelectedIndexes.indexOf(i) > -1) { this.arrLayers[i].layerData.isSelected = true; this.arrLayers[i].isSelected = true; this.triggerLayerBinding(i); arrSelectedLayers.push(i); arrCurrentElementsSelected.push(this.arrLayers[i].layerData.stageElement); } } // Store the selected layer information this.currentLayersSelected = arrSelectedLayers; this.currentElementsSelected = arrCurrentElementsSelected; // Tell the repetition what has been selected this.layerRepetition.selectedIndexes = arrSelectedIndexes; // Finally, reset the master duration. this.resetMasterDuration(); } }, // Get the indexes of layers that should be selected from // the elements that are currently selected on stage. getSelectedLayerIndexesFromStage: { value: function() { var arrIndexes = [], i = 0, j = 0, arrLayersLength = this.arrLayers.length, selectedElementsLength = this.application.ninja.selectedElements.length; for (i = 0; i < selectedElementsLength; i++) { var currentTestElement = this.application.ninja.selectedElements[i]; for (j = 0; j < arrLayersLength; j++) { if (this.arrLayers[j].layerData.stageElement == currentTestElement) { arrIndexes.push(j); } } } return arrIndexes; } }, // Update the selected layers based on what is selected on stage updateLayerSelection: { value: function() { var arrIndexes = this.getSelectedLayerIndexesFromStage(); this.selectLayers(arrIndexes); } }, // Update stage selection based on what layers are selected updateStageSelection: { value: function() { var arrSelectedElements = [], i = 0, arrLayersLength = this.arrLayers.length; // Get the selected layers for (i = 0; i < arrLayersLength; i++) { if (this.arrLayers[i].layerData.isSelected === true) { arrSelectedElements.push(this.arrLayers[i].layerData.stageElement); } } // Select the layers, or clear the selection if none were found if (arrSelectedElements.length > 0) { this.application.ninja.selectionController.selectElements(arrSelectedElements); } else { this.application.ninja.selectionController.executeSelectElement(); } } }, deselectTweens:{ value:function () { for (var i = 0; i < this.selectedTweens.length; i++) { this.selectedTweens[i].deselectTween(); } this.selectedTweens = null; this.selectedTweens = new Array(); } }, timelineLeftPanelMousedown: { value:function (event) { var ptrParent = nj.queryParentSelector(event.target, ".container-layer"), i = 0, arrLayers = document.querySelectorAll(".container-layer"), arrLayersLength = arrLayers.length, targetIndex = 0, isAlreadySelected = false, indexAlreadySelected = 0, indexLastClicked = 0; // Did the mousedown event originate within a layer? if (ptrParent === false) { // No it did not. Do nothing. return; } // Get the targetIndex, the index in the arrLayers of the // layer that was just clicked on for (i = 0; i < arrLayersLength; i++) { if (arrLayers[i] == ptrParent) { targetIndex = i; } } // Did we just click on a layer that's already selected? if (this.currentLayersSelected !== false) { indexAlreadySelected = this.currentLayersSelected.indexOf(targetIndex); } if (indexAlreadySelected > -1) { isAlreadySelected = true; } /* if (targetIndex > -1) { indexLastClicked = targetIndex; } */ // Now, do the selection based on all of that information. if (this.currentLayersSelected.length === 0) { // Nothing selected yet, so just push the new index into the array. this.currentLayersSelected.push(targetIndex); } else { // Something is already selected. What do do depends on whether // or not other keys are pressed. if (this._isControlPressed === true) { // Control key is being pressed, so we need to // either add the current layer to selectedLayers // or remove it if it's already there. if (this.currentLayersSelected === false) { this.currentLayersSelected = []; //this.currentLayerSelected = false; } if (isAlreadySelected === false) { this.currentLayersSelected.push(targetIndex); } else { this.currentLayersSelected.splice(indexAlreadySelected, 1); } this.lastLayerClicked = targetIndex; } else if (this._isShiftPressed === true) { // The shift key is being pressed. // Start by selecting the lastLayerClicked if (this.currentLayersSelected === false) { this.currentLayersSelected = []; //this.currentLayerSelected = false; } this.currentLayersSelected = [this.lastLayerClicked]; // Add all the layers between lastLayerClicked and targetIndex if (targetIndex > this.lastLayerClicked) { for (i = this.lastLayerClicked+1; i <= targetIndex; i++) { this.currentLayersSelected.push(i); } } else if (targetIndex < this.lastLayerClicked) { for (i = targetIndex; i < this.lastLayerClicked; i++) { this.currentLayersSelected.push(i); } } } else { // No key is pressed, so just select the element // and update lastLayerClicked this.currentLayersSelected = [targetIndex]; this.lastLayerClicked = targetIndex; } } //this._captureSelection = true; this.selectLayers(this.currentLayersSelected); this.updateStageSelection(); } }, timelineLeftPaneKeydown: { value: function(event) { if (event.keyCode === 16) { // Shift key has been pressed this._isShiftPressed = true; } if (event.keyCode === 17) { // Control key has been pressed this._isControlPressed = true; } } }, timelineLeftPaneKeyup: { value: function(event) { if (event.keyCode === 16) { // Shift key has been released this._isShiftPressed = false; } if (event.keyCode === 17) { // Control key has been released this._isControlPressed = false; } } }, createstageElement:{ value:function (object) { var stageElementName = "", thingToPush = this.createLayerTemplate(), myIndex = 0, i = 0, arrLayersLength = this.arrLayers.length; // Make up a layer name. this.currentLayerNumber = this.currentLayerNumber + 1; // stageElementName = "Layer " + this.currentLayerNumber; stageElementName=" "; // Possibly currentLayerNumber doesn't correctly reflect the // number of layers. Check that. // Commented out to fix WebGL rendering bug /*for(k = 0; k < arrLayersLength; k++){ if(this.arrLayers[k].layerData.layerName === stageElementName){ this.currentLayerNumber = this.currentLayerNumber + 1; stageElementName = "Layer " + this.currentLayerNumber; break; } }*/ // We will no longer have multiple things selected, so wipe that info out // if it isn't already gone. this.currentLayersSelected = false; // thingToPush is the template we just got. Now fill it in. thingToPush.layerData.layerName = stageElementName; thingToPush.layerData.layerTag = "<" + object.nodeName.toLowerCase() + ">"; thingToPush.layerData.layerID = this.currentLayerNumber; thingToPush.parentElement = this.application.ninja.currentSelectedContainer; thingToPush.layerData.isSelected = true; thingToPush.layerData._isFirstDraw = true; thingToPush.layerData.created = true; thingToPush.layerData.stageElement = object; if (this.checkable_animated.classList.contains("checked")) { thingToPush.layerData.isVisible = false; } // Determine where the new array should be inserted in arrLayers. // Ordinarily we could use this.getInsertionIndex BUT the new element // insertion and selection has already fired, so getInsertionIndex will return // incorrect info. So we need to look at the DOM. var childrenLength = this.application.ninja.currentSelectedContainer.children.length, newIndex = childrenLength -1; for (i = 0; i < childrenLength; i++) { var currTest = this.application.ninja.currentSelectedContainer.children[i]; if (object == currTest) { myIndex = newIndex - i; } } this.arrLayers.splice(myIndex, 0, thingToPush); this.selectLayers([myIndex]); } }, restoreLayer:{ value:function (ele) { var stageElementName, thingToPush = this.createLayerTemplate(); this.currentLayerNumber = this.currentLayerNumber + 1; // stageElementName = "Layer " + this.currentLayerNumber; // if(ele.dataset.storedLayerName){ // stageElementName = ele.dataset.storedLayerName; // } if(ele.id){ thingToPush.layerData.layerName = ele.id; } thingToPush.layerData.layerID = this.currentLayerNumber; thingToPush.layerData.layerTag = "<" + ele.nodeName.toLowerCase() + ">"; thingToPush.parentElement = this.application.ninja.currentSelectedContainer; if (this._openDoc) { //thingToPush.layerData.elementsList.push(ele); thingToPush.layerData.stageElement = ele; } if (this.checkable_animated.classList.contains("checked")) { thingToPush.layerData.isVisible = false; } // Are there styles to add? thingToPush.layerData.arrLayerStyles = this.createLayerStyles(); thingToPush.layerData.arrStyleTracks = this.createStyleTracks(); // Add the layer to the repetition this.temparrLayers.splice(0, 0, thingToPush); thingToPush.layerData.trackPosition = this.temparrLayers.length - 1; thingToPush.layerData.layerPosition = this.temparrLayers.length - 1; this._openDoc = false; } }, deleteLayers: { value: function(arrElements) { var i = 0, j = 0, arrLayersLength = this.arrLayers.length, arrElementsLength = arrElements.length; for (i = 0; i < arrElementsLength; i++) { var currentTest = arrElements[i]; for (j = 0; j < arrLayersLength; j++) { if (this.arrLayers[j].layerData.stageElement == currentTest) { this.arrLayers.splice(j, 1); // Super-secret magic trick: Now that we've spliced out an element, // arrLayers.length is different. We need to update it. arrLayersLength = this.arrLayers.length; } } } this.selectLayers([]); this.resetMasterDuration(); } }, resetMasterDuration:{ value:function(){ var trackDuration = 0, arrLayersLength = this.arrLayers.length, i = 0; if (arrLayersLength > 0) { for (i = 0; i < arrLayersLength; i++) { var currLength = this.arrLayers[i].layerData.trackDuration; if (currLength > trackDuration) { trackDuration = currLength; } } } this.masterDuration = trackDuration; } }, handleElementAdded:{ value:function() { this.createstageElement(this.application.ninja.selectedElements[0]); } }, handleElementsRemoved:{ value:function (event) { this.deleteLayers(event.detail); } }, handleElementReplaced:{ value:function(event){ // TODO: this needs to be updated. Not sure when an elementReplaced event will be fired? /* this.currentLayerSelected.layerData.elementsList.pop(); this.currentLayerSelected.layerData.elementsList.push(event.detail.data.newChild); this.currentLayerSelected.layerData.animatedElement = event.detail.data.newChild; */ } }, drawTimeMarkers:{ value:function () { this.timeMarkerHolder = document.createElement("div"); if(this.time_markers.children[0]){ this.time_markers.removeChild(this.time_markers.children[0]); } this.time_markers.appendChild(this.timeMarkerHolder); var i; var totalMarkers = Math.floor(this.time_markers.offsetWidth / 80); for (i = 0; i < totalMarkers; i++) { var timeMark = document.createElement("div"); var markValue = this.calculateTimeMarkerValue(i); timeMark.className = "timemark"; timeMark.innerHTML = markValue; this.timeMarkerHolder.appendChild(timeMark); } } }, calculateTimeMarkerValue:{ value:function (currentMarker) { var currentMilliseconds = currentMarker * this.millisecondsOffset; return this.convertMillisecondsToTime(currentMilliseconds); } }, updateTimeText:{ value:function (millisec) { this.timetext.innerHTML = this.convertMillisecondsToTime(millisec); } }, convertMillisecondsToTime:{ value:function(millisec){ var timeToReturn; var sec = (Math.floor((millisec / 1000))) % 60; var min = (Math.floor((millisec / 1000) / 60)) % 60; var milliSeconds = String(Math.round(millisec / 10)); var returnMillisec = milliSeconds.slice(milliSeconds.length - 2, milliSeconds.length); var returnSec; var returnMin; if (sec < 10) { returnSec = "0" + sec; } else { returnSec = sec; } if (min < 10) { returnMin = "0" + min; } else { returnMin = min; } if (returnMillisec == "0") { returnMillisec = "0" + returnMillisec; } timeToReturn = returnMin + ":" + returnSec + ":" + returnMillisec; return timeToReturn; } }, selectLayer:{ value:function (layerIndex, userSelection) { //console.log("=----> Please update this component to use TimelinePanel.selectLayers. See this message for syntax. <----="); this.selectLayers([layerIndex]); if (userSelection === true) { this.updateStageSelection(); } } }, // Get the index where a layer should be inserted based on selection. // If nothing is selected, returns false. // Used by ElementController.addElement. getInsertionIndex: { value: function() { var i = 0, currentLayersSelectedLength = this.currentLayersSelected.length, arrLayersLength = this.arrLayers.length, returnVal = arrLayersLength -1; if (this.currentLayersSelected === false) { return false; } for (i = 0; i < arrLayersLength; i++) { if (this.arrLayers[i].layerData.isSelected) { returnVal = i; } } return returnVal; } }, getLayerIndexByID:{ value:function (layerID, tempArr) { var i = 0, returnVal = false, arrLayersLength = this.arrLayers.length; if (tempArr) { var tempArrLength = this.temparrLayers.length; for (i = 0; i < tempArrLength; i++) { if (this.temparrLayers[i].layerData.layerID === layerID) { returnVal = i; } } } else { for (i = 0; i < arrLayersLength; i++) { if (this.arrLayers[i].layerData.layerID === layerID) { returnVal = i; } } } return returnVal; } }, enablePanel:{ value:function (boolEnable) { if (boolEnable) { this.timeline_disabler.style.display = "none"; } else { this.timeline_disabler.style.display = "block"; } } }, handleConfigButtonClick: { value: function(event) { event.stopPropagation(); this.handleCheckableClick(event); } }, handleDocumentClick: { value: function(event) { if (this.tl_configbutton.classList.contains("checked")) { this.tl_configbutton.classList.remove("checked"); } } }, handleAnimatedClick: { value: function(event) { if (typeof(this.application.ninja.currentDocument) === "undefined") { return; } if (this.application.ninja.currentDocument == null) { return; } this.handleCheckableClick(event); this.application.ninja.currentDocument.boolShowOnlyAnimated = event.currentTarget.classList.contains("checked"); var boolHide = false, i = 0, arrLayersLength = this.arrLayers.length; if (event.currentTarget.classList.contains("checked")) { // Hide layers with isAnimated = false; boolHide = true; } for (i = 0; i < arrLayersLength; i++) { if (boolHide) { // Hide layers with isAnimated = false if (this.arrLayers[i].layerData.isTrackAnimated === false) { this.arrLayers[i].layerData.isVisible = false; this.triggerLayerBinding(i); } } else { this.arrLayers[i].layerData.isVisible = true; this.triggerLayerBinding(i); } } } }, handleRelativeClick: { value: function(event) { if (!event.currentTarget.classList.contains("checked")) { this.handleCheckableClick(event); } this.checkable_absolute.classList.remove("checked"); this.useAbsolutePosition = false; } }, handleAbsoluteClick: { value: function(event) { if (!event.currentTarget.classList.contains("checked")) { this.handleCheckableClick(event); } this.checkable_relative.classList.remove("checked"); this.useAbsolutePosition = true; } }, handleCheckableClick: { value: function(event) { if (event.currentTarget.classList.contains("checked")) { event.currentTarget.classList.remove("checked"); } else { event.currentTarget.classList.add("checked"); } } }, // A layer's ID has been updated in the property panel. We need to update // our layer. handleLayerIdUpdate: { value: function(event) { var i = 0, arrLayersLength = this.arrLayers.length; for (i = 0; i < arrLayersLength; i++) { var myTest = this.arrLayers[i].layerData.stageElement; if (this.application.ninja.selectedElements[0] == myTest) { this.arrLayers[i].layerData.layerName = event.detail.id; this.arrLayers[i].layerName = event.detail.id; this.triggerLayerBinding(i); } } } }, // Trigger the layer/track data binding triggerLayerBinding : { value: function(intIndex) { this.arrLayers[intIndex].layerData.triggerBinding = !this.arrLayers[intIndex].layerData.triggerBinding; } }, handleLayerDragStart : { value: function(event) { var dragIcon = document.createElement("img"); event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('Text', this.identifier); // dragIcon.src = "/images/transparent.png"; dragIcon.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAA1JREFUGFdj+P//PwMACPwC/ohfBuAAAAAASUVORK5CYII=" dragIcon.width = 1; event.dataTransfer.setDragImage(dragIcon, 0, 0); // Clone the element we're dragging this.buildDragHelper(); // Get the offset var findYOffset = function(obj) { var curleft = curtop = 0; if (obj.offsetParent) { do { curleft += obj.offsetLeft; curtop += obj.offsetTop; } while (obj = obj.offsetParent); } return curtop; } this._dragAndDropHelperOffset = findYOffset(this.container_layers); this._appendHelper = true; this._deleteHelper = false; } }, buildDragHelper: { value: function() { var myContainer = document.createElement("div"), i = 0, currentLayersSelectedLength = this.currentLayersSelected.length; for (i = 0; i < currentLayersSelectedLength; i++) { var currentClone = this.layerRepetition.childComponents[this.currentLayersSelected[i]].element.cloneNode(true); currentClone.classList.add("layerSelected"); myContainer.appendChild(currentClone); this._dragLayerIndexes.push(this.currentLayersSelected[i]); } this._dragAndDropHelper = myContainer; this._dragAndDropHelper.style.opacity = 0.8; this._dragAndDropHelper.style.position = "absolute"; this._dragAndDropHelper.style.top = "0px"; this._dragAndDropHelper.style.left = "0px"; this._dragAndDropHelper.style.zIndex = 700; this._dragAndDropHelper.style.width = window.getComputedStyle(this.container_layers, null).getPropertyValue("width"); this._dragAndDropHelper.classList.add("timeline-dnd-helper"); } }, handleLayerDragover: { value: function(event) { // If this isn't a layer event we don't do anything. if (this.draggingType !== "layer") { return; } var currPos = 0, myScrollTest = ((event.y - (this._dragAndDropHelperOffset - this.user_layers.scrollTop)) + 28) - this.user_layers.scrollTop; if ((myScrollTest < 60) && (this.user_layers.scrollTop >0)) { this._scrollTracks = (this.user_layers.scrollTop - 10) } if ((myScrollTest < 50) && (this.user_layers.scrollTop >0)) { this._scrollTracks = (this.user_layers.scrollTop - 20) } if ((myScrollTest > (this.user_layers.clientHeight + 10))) { this._scrollTracks = (this.user_layers.scrollTop + 10) } if ((myScrollTest > (this.user_layers.clientHeight + 20))) { this._scrollTracks = (this.user_layers.scrollTop + 20) } currPos = event.y - (this._dragAndDropHelperOffset - this.user_layers.scrollTop)- 28; this._dragAndDropHelperCoords = currPos + "px"; this.needsDraw = true; } }, handleLayerDragEnd : { value: function(event) { // If this isn't a layer event we don't do anything. if (this.draggingType !== "layer") { return; } this._deleteHelper = true; this.needsDraw = true; } }, handleLayerDrop : { value: function(event) { // If this isn't a layer event we don't do anything. if (this.draggingType !== "layer") { return; } event.stopPropagation(); event.preventDefault(); this._deleteHelper = true; this.needsDraw = true; } }, // Keyframe drag-and-drop handleKeyframeDragover: { value: function(event) { // If this isn't a keyframe drag and drop event, we don't want to do anything. if (this.draggingType !== "keyframe") { return; } event.preventDefault(); var currPos = 0; currPos = (event.x + this.layout_tracks.scrollLeft) - 277; // Prevent dragging beyond previous or next keyframe, if any if (currPos < this.trackRepetition.childComponents[this.draggingTrackId]._keyframeMinPosition) { currPos = this.trackRepetition.childComponents[this.draggingTrackId]._keyframeMinPosition; } if (currPos > this.trackRepetition.childComponents[this.draggingTrackId]._keyframeMaxPosition) { currPos = this.trackRepetition.childComponents[this.draggingTrackId]._keyframeMaxPosition; } // Automatic scrolling when dragged to edge of window if (currPos < (this.layout_tracks.scrollLeft + 10)) { this._scrollTracks = (this.layout_tracks.scrollLeft -10); this.needsDraw = true; } if (currPos > (this.layout_tracks.offsetWidth + this.layout_tracks.scrollLeft - 20)) { this._scrollTracks = (this.layout_tracks.scrollLeft +10); this.needsDraw = true; } // Set up values in appropriate track and set that track to draw. this.trackRepetition.childComponents[this.draggingTrackId].dragAndDropHelperCoords = currPos + "px"; this.trackRepetition.childComponents[this.draggingTrackId].needsDraw = true; return false; } }, handleKeyframeDrop: { value: function(event) { // If this isn't a keyframe drop event, we don't want to do anything. if (this.draggingType !== "keyframe") { return; } event.stopPropagation(); var currPos = (event.x + this.layout_tracks.scrollLeft) - 277, currentMillisecPerPixel = Math.floor(this.millisecondsOffset / 80), currentMillisec = 0, i = 0, trackIndex = this.draggingTrackId, tweenIndex = this.trackRepetition.childComponents[trackIndex].draggingIndex; // Make sure drop happens between previous and next keyframe, if any. if (currPos < this.trackRepetition.childComponents[trackIndex]._keyframeMinPosition) { currPos = this.trackRepetition.childComponents[trackIndex]._keyframeMinPosition + 3; } if (currPos > this.trackRepetition.childComponents[trackIndex]._keyframeMaxPosition) { currPos = this.trackRepetition.childComponents[trackIndex]._keyframeMaxPosition + 3; } // Calculate the millisecond values, set repetitions, and update the rule. currentMillisec = currentMillisecPerPixel * currPos; this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex].tweenData.spanWidth = currPos - this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex - 1].tweenData.keyFramePosition; this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex].tweenData.keyFramePosition = currPos; this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex].tweenData.keyFrameMillisec = currentMillisec; this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex].tweenData.spanPosition = currPos - this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex].tweenData.spanWidth; this.trackRepetition.childComponents[trackIndex].tweenRepetition.childComponents[tweenIndex].setData(); if (tweenIndex < this.trackRepetition.childComponents[trackIndex].tweens.length -1) { var spanWidth = this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex +1].tweenData.keyFramePosition - currPos, spanPosition = currPos; this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex +1].tweenData.spanWidth = spanWidth; this.trackRepetition.childComponents[trackIndex].tweens[tweenIndex +1].tweenData.spanPosition = currPos; this.trackRepetition.childComponents[trackIndex].tweenRepetition.childComponents[tweenIndex+1].setData(); } this.trackRepetition.childComponents[trackIndex].tweenRepetition.childComponents[tweenIndex].selectTween(); this.trackRepetition.childComponents[trackIndex].tweenRepetition.childComponents[tweenIndex].keyframe.selectKeyframe(); this.trackRepetition.childComponents[trackIndex].updateKeyframeRule(); // If this is the last keyframe, we'll need to update the track duration if (tweenIndex === (this.trackRepetition.childComponents[trackIndex].tweens.length-1)) { this.arrLayers[trackIndex].layerData.trackDuration = currentMillisec; this.resetMasterDuration(); } return false; } }, /* === END: Controllers === */ /* === BEGIN: Logging routines === */ _boolDebug:{ enumerable:false, value:false // set to true to enable debugging to console; false for turning off all debugging. }, boolDebug:{ get:function () { return this._boolDebug; }, set:function (boolDebugSwitch) { this._boolDebug = boolDebugSwitch; } }, log:{ value:function (strMessage) { if (this.boolDebug) { console.log(this.getLineNumber() + ": " + strMessage); } } }, getLineNumber:{ value:function () { try { throw new Error('bazinga') } catch (e) { return e.stack.split("at")[3].split(":")[2]; } } } /* === END: Logging routines === */ });