Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Commit

Permalink
Animate inline editors open.
Browse files Browse the repository at this point in the history
  • Loading branch information
njx committed Jul 11, 2013
1 parent 73ca611 commit 119b4ef
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 61 deletions.
185 changes: 140 additions & 45 deletions src/editor/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ define(function (require, exports, module) {
Strings = require("strings"),
TextRange = require("document/TextRange").TextRange,
TokenUtils = require("utils/TokenUtils"),
ViewUtils = require("utils/ViewUtils");
ViewUtils = require("utils/ViewUtils"),
Async = require("utils/Async");

var defaultPrefs = { useTabChar: false, tabSize: 4, spaceUnits: 4, closeBrackets: false,
showLineNumbers: true, styleActiveLine: false, wordWrap: true };
Expand Down Expand Up @@ -992,29 +993,63 @@ define(function (require, exports, module) {
* @param {!{line:number, ch:number}} pos Position in text to anchor the inline.
* @param {!InlineWidget} inlineWidget The widget to add.
* @param {boolean=} scrollLineIntoView Scrolls the associated line into view. Default true.
* @return {$.Promise} A promise object that is resolved when the widget has been added (but might
* still be animating open). Never rejected.
*/
Editor.prototype.addInlineWidget = function (pos, inlineWidget, scrollLineIntoView) {
var self = this,
queue = this._inlineWidgetQueues[pos.line],
deferred = new $.Deferred();
if (!queue) {
queue = new Async.PromiseQueue();
this._inlineWidgetQueues[pos.line] = queue;
}
queue.add(function () {
self._addInlineWidgetInternal(pos, inlineWidget, scrollLineIntoView, deferred);
return deferred.promise();
});
return deferred.promise();
};

/**
* @private
* Does the actual work of addInlineWidget().
*/
Editor.prototype._addInlineWidgetInternal = function (pos, inlineWidget, scrollLineIntoView, deferred) {
var self = this;

this.removeAllInlineWidgetsForLine(pos.line);

if (scrollLineIntoView === undefined) {
scrollLineIntoView = true;
}
this.removeAllInlineWidgetsForLine(pos.line).done(function () {
if (scrollLineIntoView === undefined) {
scrollLineIntoView = true;
}

if (scrollLineIntoView) {
self._codeMirror.scrollIntoView(pos);
}

inlineWidget.info = self._codeMirror.addLineWidget(pos.line, inlineWidget.htmlContent,
{ coverGutter: true, noHScroll: true });
CodeMirror.on(inlineWidget.info.line, "delete", function () {
self._removeInlineWidgetInternal(inlineWidget);
});
self._inlineWidgets.push(inlineWidget);

if (scrollLineIntoView) {
this._codeMirror.scrollIntoView(pos);
}
// Set up the widget to start closed, then animate open when its initial height is set.
inlineWidget.$htmlContent
.height(0)
.addClass("animating")
.one("webkitTransitionEnd", function () {
inlineWidget.$htmlContent.removeClass("animating");
deferred.resolve();
});

inlineWidget.info = this._codeMirror.addLineWidget(pos.line, inlineWidget.htmlContent,
{ coverGutter: true, noHScroll: true });
CodeMirror.on(inlineWidget.info.line, "delete", function () {
self._removeInlineWidgetInternal(inlineWidget);
// Callback to widget once parented to the editor. The widget should call back to
// setInlineWidgetHeight() in order to set its initial height and animate open.
// TODO: Should we make onAdded() return the desired height so that it's a
// required part of the API, instead of people just having to know that they
// need to call setInlineWidgetHeight() at least once?
inlineWidget.onAdded();
});
this._inlineWidgets.push(inlineWidget);

// Callback to widget once parented to the editor
inlineWidget.onAdded();
};

/**
Expand All @@ -1024,20 +1059,40 @@ define(function (require, exports, module) {
// copy the array because _removeInlineWidgetInternal will modify the original
var widgets = [].concat(this.getInlineWidgets());

widgets.forEach(function (widget) {
this.removeInlineWidget(widget);
}, this);
return Async.doInParallel(
widgets,
this.removeInlineWidget.bind(this)
);
};

/**
* Removes the given inline widget.
* @param {number} inlineWidget The widget to remove.
*/
Editor.prototype.removeInlineWidget = function (inlineWidget) {
var lineNum = this._getInlineWidgetLineNumber(inlineWidget);

this._codeMirror.removeLineWidget(inlineWidget.info);
this._removeInlineWidgetInternal(inlineWidget);
if (!inlineWidget.closePromise) {
var lineNum = this._getInlineWidgetLineNumber(inlineWidget),
deferred = new $.Deferred(),
self = this;

// Remove the inline widget from our internal list immediately, so
// everyone external to us knows it's essentially already gone. We
// don't want to wait until it's done animating closed (but we do want
// the other stuff in _removeInlineWidgetInternal to wait until then).
self._removeInlineWidgetFromList(inlineWidget);

inlineWidget.$htmlContent.addClass("animating")
.one("webkitTransitionEnd", function () {
inlineWidget.$htmlContent.removeClass("animating");
self._codeMirror.removeLineWidget(inlineWidget.info);
self._removeInlineWidgetInternal(inlineWidget);
deferred.resolve();
})
.height(0);

inlineWidget.closePromise = deferred.promise();
}
return inlineWidget.closePromise;
};

/**
Expand All @@ -1056,29 +1111,47 @@ define(function (require, exports, module) {
return w.info;
});

widgetInfos.forEach(function (info) {
// Lookup the InlineWidget object using the same index
inlineWidget = self._inlineWidgets[allWidgetInfos.indexOf(info)];
self.removeInlineWidget(inlineWidget);
});

return Async.doInParallel(
widgetInfos,
function (info) {
// Lookup the InlineWidget object using the same index
inlineWidget = self._inlineWidgets[allWidgetInfos.indexOf(info)];
if (inlineWidget) {
return self.removeInlineWidget(inlineWidget);
} else {
return new $.Deferred().resolve().promise();
}
}
);
} else {
return new $.Deferred().resolve().promise();
}
};

/**
* Cleans up the given inline widget from our internal list of widgets. It's okay
* to call this multiple times for the same widget--it will just do nothing if
* the widget has already been removed.
* @param {InlineWidget} inlineWidget an inline widget.
*/
Editor.prototype._removeInlineWidgetFromList = function (inlineWidget) {
var i;
var l = this._inlineWidgets.length;
for (i = 0; i < l; i++) {
if (this._inlineWidgets[i] === inlineWidget) {
this._inlineWidgets.splice(i, 1);
break;
}
}
};

/**
* Cleans up the given inline widget from our internal list of widgets.
* @param {number} inlineId id returned by addInlineWidget().
* Removes the inline widget from the editor and notifies it to clean itself up.
* @param {InlineWidget} inlineWidget an inline widget.
*/
Editor.prototype._removeInlineWidgetInternal = function (inlineWidget) {
if (!inlineWidget.isClosed) {
var i;
var l = this._inlineWidgets.length;
for (i = 0; i < l; i++) {
if (this._inlineWidgets[i] === inlineWidget) {
this._inlineWidgets.splice(i, 1);
break;
}
}
this._removeInlineWidgetFromList(inlineWidget);
inlineWidget.onClosed();
inlineWidget.isClosed = true;
}
Expand Down Expand Up @@ -1119,14 +1192,30 @@ define(function (require, exports, module) {
changed = (oldHeight !== height),
isAttached = inlineWidget.info !== undefined;

function updateHeight() {
// Notify CodeMirror for the height change.
if (isAttached) {
inlineWidget.info.changed();
}
}

function setOuterHeight() {
$(node).height(height);
if ($(node).hasClass("animating")) {
$(node).one("webkitTransitionEnd", updateHeight);
} else {
updateHeight();
}
}

// Make sure we set an explicit height on the widget, so children can use things like
// min-height if they want.
if (changed || !node.style.height) {
$(node).height(height);

if (isAttached) {
// Notify CodeMirror for the height change
inlineWidget.info.changed();
// If we're animating, set the wrapper's height on a timeout so the layout is finished before we animate.
if ($(node).hasClass("animating")) {
window.setTimeout(setOuterHeight, 0);
} else {
setOuterHeight();
}
}

Expand Down Expand Up @@ -1324,6 +1413,12 @@ define(function (require, exports, module) {
*/
Editor.prototype._visibleRange = null;

/**
* @private
* @type {Object}
* Promise queues for inline widgets being added to a given line.
*/
Editor.prototype._inlineWidgetQueues = {};

// Global settings that affect all Editor instances (both currently open Editors as well as those created
// in the future)
Expand Down
20 changes: 11 additions & 9 deletions src/editor/EditorManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,10 @@ define(function (require, exports, module) {
// If one of them will provide a widget, show it inline once ready
if (inlinePromise) {
inlinePromise.done(function (inlineWidget) {
editor.addInlineWidget(pos, inlineWidget);
PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
result.resolve();
editor.addInlineWidget(pos, inlineWidget).done(function () {
PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
result.resolve();
});
}).fail(function () {
// terminate timer that was started above
PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
Expand All @@ -192,6 +193,7 @@ define(function (require, exports, module) {
* is). The widget's onClosed() callback will be run as a result.
* @param {!Editor} hostEditor The editor containing the widget.
* @param {!InlineWidget} inlineWidget The inline widget to close.
* @return {$.Promise} A promise that's resolved when the widget is fully closed.
*/
function closeInlineWidget(hostEditor, inlineWidget) {
// If widget has focus, return it to the hostEditor & move the cursor to where the inline used to be
Expand All @@ -207,7 +209,7 @@ define(function (require, exports, module) {
hostEditor.focus();
}

hostEditor.removeInlineWidget(inlineWidget);
return hostEditor.removeInlineWidget(inlineWidget);
}

/**
Expand Down Expand Up @@ -670,11 +672,11 @@ define(function (require, exports, module) {
if (inlineWidget) {
// an inline widget's editor has focus, so close it
PerfUtils.markStart(PerfUtils.INLINE_WIDGET_CLOSE);
inlineWidget.close();
PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_CLOSE);

// return a resolved promise to CommandManager
result.resolve(false);
inlineWidget.close().done(function () {
PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_CLOSE);
// return a resolved promise to CommandManager
result.resolve(false);
});
} else {
// main editor has focus, so create an inline editor
_openInlineWidget(_currentEditor, providers).done(function () {
Expand Down
3 changes: 2 additions & 1 deletion src/editor/InlineWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ define(function (require, exports, module) {

/**
* Closes this inline widget and all its contained Editors
* @return {$.Promise} A promise that's resolved when the widget is fully closed.
*/
InlineWidget.prototype.close = function () {
EditorManager.closeInlineWidget(this.hostEditor, this);
return EditorManager.closeInlineWidget(this.hostEditor, this);
// closeInlineWidget() causes our onClosed() handler to be called
};

Expand Down
15 changes: 14 additions & 1 deletion src/editor/MultiRangeInlineEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,19 @@ define(function (require, exports, module) {
* @override
*/
MultiRangeInlineEditor.prototype.onAdded = function () {
var self = this;

// Before setting the inline widget height, force a height on the
// floating related-container in order for CodeMirror to layout and
// compute scrollbars
this.$relatedContainer.height(this.$related.height());

// Update the position of the selected marker now that we're laid out, and then
// set it to animate for future updates.
this._updateSelectedMarker().done(function () {
self.$selectedMarker.addClass("animate");
});

// Call super
MultiRangeInlineEditor.prototype.parentClass.onAdded.apply(this, arguments);

Expand Down Expand Up @@ -302,7 +310,8 @@ define(function (require, exports, module) {
};

MultiRangeInlineEditor.prototype._updateSelectedMarker = function () {
var $rangeItem = this._ranges[this._selectedRangeIndex].$listItem;
var result = new $.Deferred(),
$rangeItem = this._ranges[this._selectedRangeIndex].$listItem;

// scroll the selection to the rangeItem, use setTimeout to wait for DOM updates
var self = this;
Expand All @@ -329,7 +338,11 @@ define(function (require, exports, module) {
self.$relatedContainer.scrollTop(itemBottom - containerHeight);
}
}

result.resolve();
}, 0);

return result.promise();
};

/**
Expand Down
Loading

0 comments on commit 119b4ef

Please sign in to comment.