Skip to content

Commit 311a2f8

Browse files
committed
feat: add woosh-to-target close/open animation for dialogs
Add closeToElement parameter to showModalDialogUsingTemplate() that animates dialogs opening from and closing toward a target element. The new project dialog uses this to woosh to/from the #newProject button, replacing the old notification popup with a natural visual cue. - Remove _showNewProjectNotification() from guided-tour.js - Remove unused NEW_PROJECT_NOTIFICATION string and new_project.png - Add landing pulse animation on target element after close
1 parent 207826a commit 311a2f8

6 files changed

Lines changed: 110 additions & 32 deletions

File tree

src/extensionsIntegrated/Phoenix/guided-tour.js

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -89,31 +89,6 @@ define(function (require, exports, module) {
8989
editorContextMenu.on(Menus.EVENT_BEFORE_CONTEXT_MENU_OPEN, _showNotification);
9090
}
9191

92-
// 3. When user changes file by clicking on files panel, we show "click here to open new project window"
93-
// Only shown once.
94-
function _showNewProjectNotification() {
95-
if(userAlreadyDidAction.newProjectShown){
96-
return;
97-
}
98-
if(currentlyShowingNotification){
99-
setTimeout(_showNewProjectNotification, NOTIFICATION_BACKOFF);
100-
return;
101-
}
102-
userAlreadyDidAction.newProjectShown = true;
103-
PhStore.setItem(GUIDED_TOUR_LOCAL_STORAGE_KEY, JSON.stringify(userAlreadyDidAction));
104-
Metrics.countEvent(Metrics.EVENT_TYPE.UI, "guide", "newProj");
105-
currentlyShowingNotification = NotificationUI.createFromTemplate(
106-
Strings.START_PROJECT, Strings.NEW_PROJECT_NOTIFICATION,
107-
"newProject", {
108-
allowedPlacements: ['top', 'bottom'],
109-
autoCloseTimeS: 15,
110-
dismissOnClick: true}
111-
);
112-
currentlyShowingNotification.done(()=>{
113-
currentlyShowingNotification = null;
114-
});
115-
}
116-
11792
function _showFirstUseSurvey(surveyURL, delayOverride, title, useDialog) {
11893
let surveyVersion = 6; // increment this if you want to show this again
11994
if(userAlreadyDidAction.generalSurveyShownVersion === surveyVersion) {
@@ -302,7 +277,6 @@ define(function (require, exports, module) {
302277
return;
303278
}
304279
tourStarted = true;
305-
_showNewProjectNotification();
306280
_showBeautifyNotification();
307281
_showSurveys();
308282
};

src/extensionsIntegrated/Phoenix/new-project.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ define(function (require, exports, module) {
8585
newProjectURL: `${window.Phoenix.baseURL}assets/new-project/code-editor.html`
8686
};
8787
let dialogueContents = Mustache.render(newProjectTemplate, templateVars);
88-
newProjectDialogueObj = Dialogs.showModalDialogUsingTemplate(dialogueContents, true);
88+
newProjectDialogueObj = Dialogs.showModalDialogUsingTemplate(dialogueContents, true, "#newProject");
8989
_focusContentWindow();
9090
Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "dialogue", "open");
9191
return newProjectDialogueObj;

src/nls/root/strings.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1336,7 +1336,6 @@ define({
13361336
"PREVIEW": "Preview",
13371337
"BUILD_WEBSITE": "Build Website",
13381338
"VIEW_MORE": "View More...",
1339-
"NEW_PROJECT_NOTIFICATION": "Click this icon to open the `Start Project` window again.</br> See Recent Projects, Open Folder or start projects from templates.</br> <img src=\"styles/images/new_project.png\">",
13401339
"BEAUTIFY_CODE_NOTIFICATION": "Click here or press <b>`{0}`</b> to beautify code. </br> <img src=\"styles/images/beautify.gif\">",
13411340
"DEFAULT_PROJECT_NOTIFICATION": "Open the <b>Default Project</b> in {APP_NAME} to get started quickly (ideal as a scratch pad).</br></br>Or, open a folder from your computer with the <strong>Open Folder</strong> icon below.<br><a href='#' style='float:right;'>ok</a>",
13421341
"DIRECTORY_REPLACE_MESSAGE": "The selected folder <span class='dialog-filename'>{0}</span> is not empty. Are you sure you want to replace the folder contents with the project?",

src/styles/brackets_patterns_override.less

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,18 @@ a:focus {
10461046
vertical-align: middle;
10471047
}
10481048

1049+
@keyframes dialog-woosh-land-pulse {
1050+
0% { transform: scale(1); }
1051+
25% { transform: scale(1.5); }
1052+
50% { transform: scale(0.9); }
1053+
75% { transform: scale(1.15); }
1054+
100% { transform: scale(1); }
1055+
}
1056+
1057+
.dialog-woosh-land {
1058+
animation: dialog-woosh-land-pulse 600ms ease-in-out;
1059+
}
1060+
10491061
.modal {
10501062
background-color: @bc-panel-bg;
10511063
border: 1px solid @bc-panel-border;

src/styles/images/new_project.png

-2.83 KB
Binary file not shown.

src/widgets/Dialogs.js

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,51 @@ define(function (require, exports, module) {
127127
*/
128128
function _dismissDialog($dlg, buttonId) {
129129
$dlg.data("buttonId", buttonId);
130-
$dlg.modal("hide");
131130

132-
if(!_isAnyDialogShown() && EditorManager.getActiveEditor()){
133-
EditorManager.getActiveEditor().focus();
131+
let closeToElement = $dlg.data("closeToElement");
132+
let $target = closeToElement ? $(closeToElement) : null;
133+
134+
if ($target && $target.length && $target.is(":visible")) {
135+
let targetRect = $target[0].getBoundingClientRect();
136+
let targetCenterX = targetRect.left + targetRect.width / 2;
137+
let targetCenterY = targetRect.top + targetRect.height / 2;
138+
139+
let $modal = $dlg;
140+
let modalRect = $modal[0].getBoundingClientRect();
141+
let modalCenterX = modalRect.left + modalRect.width / 2;
142+
let modalCenterY = modalRect.top + modalRect.height / 2;
143+
144+
let translateX = targetCenterX - modalCenterX;
145+
let translateY = targetCenterY - modalCenterY;
146+
147+
$modal.css({
148+
"animation": "none",
149+
"transition": "transform 300ms ease-in, opacity 300ms ease-in",
150+
"transform": `translate(${translateX}px, ${translateY}px) scale(0.05)`,
151+
"opacity": "0"
152+
});
153+
154+
let $backdrop = $modal.next(".modal-backdrop");
155+
$backdrop.css({
156+
"transition": "opacity 300ms ease-in",
157+
"opacity": "0"
158+
});
159+
160+
setTimeout(function () {
161+
$dlg.modal("hide");
162+
if (!_isAnyDialogShown() && EditorManager.getActiveEditor()) {
163+
EditorManager.getActiveEditor().focus();
164+
}
165+
$target.addClass("dialog-woosh-land");
166+
setTimeout(function () {
167+
$target.removeClass("dialog-woosh-land");
168+
}, 600);
169+
}, 300);
170+
} else {
171+
$dlg.modal("hide");
172+
if (!_isAnyDialogShown() && EditorManager.getActiveEditor()) {
173+
EditorManager.getActiveEditor().focus();
174+
}
134175
}
135176
}
136177

@@ -361,9 +402,12 @@ define(function (require, exports, module) {
361402
* @param {boolean=} autoDismiss Whether to automatically dismiss the dialog when one of the buttons
362403
* is clicked. Default true. If false, you'll need to manually handle button clicks and the Esc
363404
* key, and dismiss the dialog yourself when ready by calling `close()` on the returned dialog.
405+
* @param {string=} closeToElement A CSS selector for a target element. If specified, the dialog
406+
* will animate open from and close toward the target element (a "woosh" effect). If the target
407+
* is not found or not visible, the dialog opens/closes normally.
364408
* @return {Dialog}
365409
*/
366-
function showModalDialogUsingTemplate(template, autoDismiss) {
410+
function showModalDialogUsingTemplate(template, autoDismiss, closeToElement) {
367411
if (autoDismiss === undefined) {
368412
autoDismiss = true;
369413
}
@@ -382,6 +426,10 @@ define(function (require, exports, module) {
382426
// Save the dialog promise for unit tests
383427
$dlg.data("promise", promise);
384428

429+
if (closeToElement) {
430+
$dlg.data("closeToElement", closeToElement);
431+
}
432+
385433
let keydownHook = function (e) {
386434
return _keydownHook.call($dlg, e, autoDismiss);
387435
};
@@ -435,6 +483,51 @@ define(function (require, exports, module) {
435483

436484
// Push our global keydown handler onto the global stack of handlers.
437485
KeyBindingManager.addGlobalKeydownHook(keydownHook);
486+
487+
// Animate open from target element if closeToElement is set
488+
let openFromEl = $dlg.data("closeToElement");
489+
let $openTarget = openFromEl ? $(openFromEl) : null;
490+
if ($openTarget && $openTarget.length && $openTarget.is(":visible")) {
491+
let targetRect = $openTarget[0].getBoundingClientRect();
492+
let targetCenterX = targetRect.left + targetRect.width / 2;
493+
let targetCenterY = targetRect.top + targetRect.height / 2;
494+
495+
let modalRect = $dlg[0].getBoundingClientRect();
496+
let modalCenterX = modalRect.left + modalRect.width / 2;
497+
let modalCenterY = modalRect.top + modalRect.height / 2;
498+
499+
let translateX = targetCenterX - modalCenterX;
500+
let translateY = targetCenterY - modalCenterY;
501+
502+
// Disable the default modal CSS animation so our woosh takes over
503+
// Start at the target position, scaled down
504+
$dlg.css({
505+
"animation": "none",
506+
"transition": "none",
507+
"transform": `translate(${translateX}px, ${translateY}px) scale(0.05)`,
508+
"opacity": "0"
509+
});
510+
511+
let $backdrop = $dlg.next(".modal-backdrop");
512+
$backdrop.css({
513+
"transition": "none",
514+
"opacity": "0"
515+
});
516+
517+
// Force reflow then animate to natural position
518+
// eslint-disable-next-line no-unused-expressions
519+
$dlg[0].offsetHeight;
520+
521+
$dlg.css({
522+
"transition": "transform 300ms ease-out, opacity 300ms ease-out",
523+
"transform": "translate(0, 0) scale(1)",
524+
"opacity": "1"
525+
});
526+
$backdrop.css({
527+
"transition": "opacity 300ms ease-out",
528+
"opacity": ""
529+
});
530+
}
438531
});
439532

440533
// Click handler for buttons

0 commit comments

Comments
 (0)