diff --git a/src/components/dialoghelper/dialoghelper.js b/src/components/dialogHelper/dialogHelper.js similarity index 100% rename from src/components/dialoghelper/dialoghelper.js rename to src/components/dialogHelper/dialogHelper.js diff --git a/src/components/dialogHelper/dialoghelper.css b/src/components/dialogHelper/dialoghelper.css new file mode 100644 index 0000000000..2cc20b5ff2 --- /dev/null +++ b/src/components/dialogHelper/dialoghelper.css @@ -0,0 +1,173 @@ +.dialogContainer { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 999999 !important; + contain: strict; + overflow: hidden; + overscroll-behavior: contain; +} + +.dialog { + margin: 0; + border-radius: .2em; + -webkit-font-smoothing: antialiased; + border: 0; + padding: 0; + will-change: transform, opacity; + /* Strict does not work well with actionsheet */ + contain: style paint; + box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.4); +} + +.dialog-fixedSize { + border-radius: 0; + max-height: none; + max-width: none; + contain: layout style paint; +} + +.dialog-fullscreen { + /* Needed due to formDialog style */ + position: fixed !important; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: 0; + box-shadow: none; +} + +@keyframes scaledown { + from { + opacity: 1; + transform: none; + } + + to { + opacity: 0; + transform: scale(.5); + } +} + +@keyframes scaleup { + from { + transform: scale(.5); + opacity: 0; + } + + to { + transform: none; + opacity: 1; + } +} + +@keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fadeout { + + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes slideup { + from { + opacity: 0; + transform: translate3d(0, 30%, 0); + } + + to { + opacity: 1; + transform: none; + } +} + +@keyframes slidedown { + + from { + opacity: 1; + transform: none; + } + + to { + opacity: 0; + transform: translate3d(0, 20%, 0); + } +} + +@media all and (max-width: 80em), all and (max-height: 45em) { + + .dialog-fixedSize, .dialog-fullscreen-lowres { + position: fixed !important; + top: 0 !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + margin: 0 !important; + box-shadow: none; + } +} + +@media all and (min-width: 80em) and (min-height: 45em) { + + .dialog-medium { + width: 80%; + height: 80%; + } + + .dialog-medium-tall { + width: 80%; + height: 90%; + } + + .dialog-small { + width: 60%; + height: 80%; + } + + .dialog-fullscreen-border { + width: 90%; + height: 90%; + } +} + +.noScroll { + overflow-x: hidden !important; + overflow-y: hidden !important; +} + +.dialogBackdrop { + background-color: #000; + opacity: 0; + position: fixed !important; + top: 0 !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + margin: 0 !important; + z-index: 999999 !important; + transition: opacity ease-out 0.2s; + will-change: opacity; +} + +.dialogBackdropOpened { + opacity: .5; +} diff --git a/src/components/dialogHelper/package.json b/src/components/dialogHelper/package.json new file mode 100644 index 0000000000..71863d045f --- /dev/null +++ b/src/components/dialogHelper/package.json @@ -0,0 +1,3 @@ +{ + "main": "dialogHelper.js" +} \ No newline at end of file diff --git a/src/components/dialoghelper/dialogHelper.js b/src/components/dialoghelper/dialogHelper.js new file mode 100644 index 0000000000..36bb23bfd7 --- /dev/null +++ b/src/components/dialoghelper/dialogHelper.js @@ -0,0 +1,486 @@ +define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', 'dom', 'css!./dialoghelper.css', 'scrollStyles'], function (appRouter, focusManager, browser, layoutManager, inputManager, dom) { + 'use strict'; + + var globalOnOpenCallback; + + function enableAnimation() { + + // too slow + if (browser.tv) { + return false; + } + + return browser.supportsCssAnimation(); + } + + function removeCenterFocus(dlg) { + + if (layoutManager.tv) { + if (dlg.classList.contains('scrollX')) { + centerFocus(dlg, true, false); + } + else if (dlg.classList.contains('smoothScrollY')) { + centerFocus(dlg, false, false); + } + } + } + + function tryRemoveElement(elem) { + var parentNode = elem.parentNode; + if (parentNode) { + + // Seeing crashes in edge webview + try { + parentNode.removeChild(elem); + } catch (err) { + console.log('Error removing dialog element: ' + err); + } + } + } + + function DialogHashHandler(dlg, hash, resolve) { + + var self = this; + self.originalUrl = window.location.href; + var activeElement = document.activeElement; + var removeScrollLockOnClose = false; + + function onHashChange(e) { + + var isBack = self.originalUrl === window.location.href; + + if (isBack || !isOpened(dlg)) { + window.removeEventListener('popstate', onHashChange); + } + + if (isBack) { + self.closedByBack = true; + closeDialog(dlg); + } + } + + function onBackCommand(e) { + + if (e.detail.command === 'back') { + self.closedByBack = true; + e.preventDefault(); + e.stopPropagation(); + closeDialog(dlg); + } + } + + function onDialogClosed() { + + if (!isHistoryEnabled(dlg)) { + inputManager.off(dlg, onBackCommand); + } + + window.removeEventListener('popstate', onHashChange); + + removeBackdrop(dlg); + dlg.classList.remove('opened'); + + if (removeScrollLockOnClose) { + document.body.classList.remove('noScroll'); + } + + if (!self.closedByBack && isHistoryEnabled(dlg)) { + var state = history.state || {}; + if (state.dialogId === hash) { + history.back(); + } + } + + if (layoutManager.tv) { + focusManager.focus(activeElement); + } + + if (dlg.getAttribute('data-removeonclose') !== 'false') { + removeCenterFocus(dlg); + + var dialogContainer = dlg.dialogContainer; + if (dialogContainer) { + tryRemoveElement(dialogContainer); + dlg.dialogContainer = null; + } else { + tryRemoveElement(dlg); + } + } + + //resolve(); + // if we just called history.back(), then use a timeout to allow the history events to fire first + setTimeout(function () { + resolve({ + element: dlg, + closedByBack: self.closedByBack + }); + }, 1); + } + + dlg.addEventListener('close', onDialogClosed); + + var center = !dlg.classList.contains('dialog-fixedSize'); + if (center) { + dlg.classList.add('centeredDialog'); + } + + dlg.classList.remove('hide'); + + addBackdropOverlay(dlg); + + dlg.classList.add('opened'); + dlg.dispatchEvent(new CustomEvent('open', { + bubbles: false, + cancelable: false + })); + + if (dlg.getAttribute('data-lockscroll') === 'true' && !document.body.classList.contains('noScroll')) { + document.body.classList.add('noScroll'); + removeScrollLockOnClose = true; + } + + animateDialogOpen(dlg); + + if (isHistoryEnabled(dlg)) { + appRouter.pushState({ dialogId: hash }, "Dialog", '#' + hash); + + window.addEventListener('popstate', onHashChange); + } else { + inputManager.on(dlg, onBackCommand); + } + } + + function addBackdropOverlay(dlg) { + + var backdrop = document.createElement('div'); + backdrop.classList.add('dialogBackdrop'); + + var backdropParent = dlg.dialogContainer || dlg; + backdropParent.parentNode.insertBefore(backdrop, backdropParent); + dlg.backdrop = backdrop; + + // trigger reflow or the backdrop will not animate + void backdrop.offsetWidth; + backdrop.classList.add('dialogBackdropOpened'); + + dom.addEventListener((dlg.dialogContainer || backdrop), 'click', function (e) { + if (e.target === dlg.dialogContainer) { + close(dlg); + } + }, { + passive: true + }); + } + + function isHistoryEnabled(dlg) { + return dlg.getAttribute('data-history') === 'true'; + } + + function open(dlg) { + + if (globalOnOpenCallback) { + globalOnOpenCallback(dlg); + } + + var parent = dlg.parentNode; + if (parent) { + parent.removeChild(dlg); + } + + var dialogContainer = document.createElement('div'); + dialogContainer.classList.add('dialogContainer'); + dialogContainer.appendChild(dlg); + dlg.dialogContainer = dialogContainer; + document.body.appendChild(dialogContainer); + + return new Promise(function (resolve, reject) { + + new DialogHashHandler(dlg, 'dlg' + new Date().getTime(), resolve); + }); + } + + function isOpened(dlg) { + + //return dlg.opened; + return !dlg.classList.contains('hide'); + } + + function close(dlg) { + + if (isOpened(dlg)) { + if (isHistoryEnabled(dlg)) { + history.back(); + } else { + closeDialog(dlg); + } + } + } + + function closeDialog(dlg) { + + if (!dlg.classList.contains('hide')) { + + dlg.dispatchEvent(new CustomEvent('closing', { + bubbles: false, + cancelable: false + })); + + var onAnimationFinish = function () { + focusManager.popScope(dlg); + + dlg.classList.add('hide'); + dlg.dispatchEvent(new CustomEvent('close', { + bubbles: false, + cancelable: false + })); + }; + + animateDialogClose(dlg, onAnimationFinish); + } + } + + function animateDialogOpen(dlg) { + + var onAnimationFinish = function () { + focusManager.pushScope(dlg); + if (dlg.getAttribute('data-autofocus') === 'true') { + focusManager.autoFocus(dlg); + } + }; + + if (enableAnimation()) { + + var onFinish = function () { + dom.removeEventListener(dlg, dom.whichAnimationEvent(), onFinish, { + once: true + }); + onAnimationFinish(); + }; + dom.addEventListener(dlg, dom.whichAnimationEvent(), onFinish, { + once: true + }); + return; + } + + onAnimationFinish(); + } + + function animateDialogClose(dlg, onAnimationFinish) { + + if (enableAnimation()) { + + var animated = true; + + switch (dlg.animationConfig.exit.name) { + + case 'fadeout': + dlg.style.animation = 'fadeout ' + dlg.animationConfig.exit.timing.duration + 'ms ease-out normal both'; + break; + case 'scaledown': + dlg.style.animation = 'scaledown ' + dlg.animationConfig.exit.timing.duration + 'ms ease-out normal both'; + break; + case 'slidedown': + dlg.style.animation = 'slidedown ' + dlg.animationConfig.exit.timing.duration + 'ms ease-out normal both'; + break; + default: + animated = false; + break; + } + var onFinish = function () { + dom.removeEventListener(dlg, dom.whichAnimationEvent(), onFinish, { + once: true + }); + onAnimationFinish(); + }; + dom.addEventListener(dlg, dom.whichAnimationEvent(), onFinish, { + once: true + }); + + if (animated) { + return; + } + } + + onAnimationFinish(); + } + + var supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style; + + function shouldLockDocumentScroll(options) { + + if (supportsOverscrollBehavior && (options.size || !browser.touch)) { + return false; + } + + if (options.lockScroll != null) { + return options.lockScroll; + } + + if (options.size === 'fullscreen') { + return true; + } + + if (options.size) { + return true; + } + + return browser.touch; + } + + function removeBackdrop(dlg) { + + var backdrop = dlg.backdrop; + + if (!backdrop) { + return; + } + + dlg.backdrop = null; + + var onAnimationFinish = function () { + tryRemoveElement(backdrop); + }; + + if (enableAnimation()) { + + backdrop.classList.remove('dialogBackdropOpened'); + + // this is not firing animatonend + setTimeout(onAnimationFinish, 300); + return; + } + + onAnimationFinish(); + } + + function centerFocus(elem, horiz, on) { + require(['scrollHelper'], function (scrollHelper) { + var fn = on ? 'on' : 'off'; + scrollHelper.centerFocus[fn](elem, horiz); + }); + } + + function createDialog(options) { + + options = options || {}; + + // If there's no native dialog support, use a plain div + // Also not working well in samsung tizen browser, content inside not clickable + // Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog + var dlg = document.createElement('div'); + + dlg.classList.add('focuscontainer'); + dlg.classList.add('hide'); + + if (shouldLockDocumentScroll(options)) { + dlg.setAttribute('data-lockscroll', 'true'); + } + + if (options.enableHistory !== false && appRouter.enableNativeHistory()) { + dlg.setAttribute('data-history', 'true'); + } + + // without this safari will scroll the background instead of the dialog contents + // but not needed here since this is already on top of an existing dialog + // but skip it in IE because it's causing the entire browser to hang + // Also have to disable for firefox because it's causing select elements to not be clickable + if (options.modal !== false) { + dlg.setAttribute('modal', 'modal'); + } + + if (options.autoFocus !== false) { + dlg.setAttribute('data-autofocus', 'true'); + } + + var defaultEntryAnimation; + var defaultExitAnimation; + + defaultEntryAnimation = 'scaleup'; + defaultExitAnimation = 'scaledown'; + var entryAnimation = options.entryAnimation || defaultEntryAnimation; + var exitAnimation = options.exitAnimation || defaultExitAnimation; + + // If it's not fullscreen then lower the default animation speed to make it open really fast + var entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280); + var exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220); + + dlg.animationConfig = { + // scale up + 'entry': { + name: entryAnimation, + timing: { + duration: entryAnimationDuration, + easing: 'ease-out' + } + }, + // fade out + 'exit': { + name: exitAnimation, + timing: { + duration: exitAnimationDuration, + easing: 'ease-out', + fill: 'both' + } + } + }; + + dlg.classList.add('dialog'); + + if (options.scrollX) { + dlg.classList.add('scrollX'); + dlg.classList.add('smoothScrollX'); + + if (layoutManager.tv) { + centerFocus(dlg, true, true); + } + } + else if (options.scrollY !== false) { + dlg.classList.add('smoothScrollY'); + + if (layoutManager.tv) { + centerFocus(dlg, false, true); + } + } + + if (options.removeOnClose) { + dlg.setAttribute('data-removeonclose', 'true'); + } + + if (options.size) { + dlg.classList.add('dialog-fixedSize'); + dlg.classList.add('dialog-' + options.size); + } + + if (enableAnimation()) { + + switch (dlg.animationConfig.entry.name) { + + case 'fadein': + dlg.style.animation = 'fadein ' + entryAnimationDuration + 'ms ease-out normal'; + break; + case 'scaleup': + dlg.style.animation = 'scaleup ' + entryAnimationDuration + 'ms ease-out normal both'; + break; + case 'slideup': + dlg.style.animation = 'slideup ' + entryAnimationDuration + 'ms ease-out normal'; + break; + case 'slidedown': + dlg.style.animation = 'slidedown ' + entryAnimationDuration + 'ms ease-out normal'; + break; + default: + break; + } + } + + return dlg; + } + + return { + open: open, + close: close, + createDialog: createDialog, + setOnOpen: function (val) { + globalOnOpenCallback = val; + } + }; +}); \ No newline at end of file