import * as nav from 'bitbucket/util/navbuilder';
import * as server from 'bitbucket/util/server';
const THEME_CONST = Object.freeze({
defaultTheme: 'LIGHT',
theme: {
LIGHT: 'LIGHT',
DARK: 'DARK',
MATCHING: 'MATCHING',
},
mediaListeners: {
DARK: '(prefers-color-scheme: dark)',
LIGHT: '(prefers-color-scheme: light)',
},
matchingAttr: 'data-color-mode-auto',
definitionAttr: 'data-theme',
colorModeAttr: 'data-color-mode',
themeClass: 'aui-theme-design-tokens',
colorMode: {
LIGHT: {
light: 'light',
dark: 'dark',
colorMode: 'light',
},
DARK: {
dark: 'dark',
light: 'light',
colorMode: 'dark',
},
MATCHING: {
light: 'light',
dark: 'dark',
colorMode: 'matching',
},
},
settings: {
payload: {
DARK: {
colorMode: 'DARK',
darkThemeKey: 'dark',
lightThemeKey: 'light',
},
LIGHT: {
colorMode: 'LIGHT',
darkThemeKey: 'dark',
lightThemeKey: 'light',
},
MATCHING: {
colorMode: 'MATCHING',
darkThemeKey: 'dark',
lightThemeKey: 'light',
},
},
type: server.method.POST,
url: nav.rest('atlassian-theme', '1').addPathComponents('user-preferences').build(),
statusCode: {
'*': false,
},
},
coverage: {
urlParam: 'web.colors',
default: '#ffab00ff',
},
events: {
BROADCAST: 'THEME_BROADCAST',
REQUEST: 'THEME_REQUEST',
RESPONSE: 'THEME_RESPONSE',
},
});
let currentTheme = null;
/**
* setThemeToAPI does an API call to set theme in user preferences:
*
* POST rest/atlassian-theme/1/user-preferences
*
* @returns {Promise} - a promise that resolves with the current theme
*/
const setThemeToAPI = (theme) => {
return new Promise((resolve, reject) => {
return server
.rest({
url: THEME_CONST.settings.url,
type: THEME_CONST.settings.type,
statusCode: THEME_CONST.settings.statusCode,
data: THEME_CONST.settings.payload[theme],
})
.then(() => resolve(theme))
.fail(reject);
});
};
const setDataTheme = (theme) => {
const colorMode = THEME_CONST.colorMode[theme];
// setGlobalTheme is not working inside an iframe, we add it manually as a safety net in case setGlobalTheme fails
// TODO: remove once setGlobalTheme works in iframe
document.documentElement.setAttribute(
THEME_CONST.definitionAttr,
`light:${colorMode.light} dark:${colorMode.dark} spacing:spacing`
);
// setGlobalTheme takes to long to set data-color-mode attributes, we add it manually to prevent
// flashing while using Match system
// TODO: remove once setGlobalTheme fixes flashing issue
document.documentElement.setAttribute(THEME_CONST.colorModeAttr, colorMode.colorMode);
return window.AJS.DesignTokens.setGlobalTheme(colorMode);
};
/**
* Sets the required attributes by Atlaskit and AUI to turn ON/OFF their Dark themes
*
* @param {Object} theme - Any of the available themes in THEME_CONST.theme
*
* @returns {string} theme - current theme
*/
const setBitbucketTheme = (theme) => {
// prevent setting the theme multiple times when in Light or dark mode
if (theme !== THEME_CONST.theme.MATCHING && currentTheme === theme) {
return;
}
currentTheme = theme;
const docElement = document.documentElement;
const body = window.document.getElementsByTagName('body')[0];
const isSystemInDarkTheme = window.matchMedia(THEME_CONST.mediaListeners.DARK).matches;
if (theme === THEME_CONST.theme.MATCHING) {
theme = isSystemInDarkTheme ? THEME_CONST.theme.DARK : THEME_CONST.theme.LIGHT;
docElement.setAttribute(THEME_CONST.matchingAttr, '');
} else {
docElement.removeAttribute(THEME_CONST.matchingAttr);
}
const globalThemePromise = setDataTheme(theme);
body.classList.add(THEME_CONST.themeClass);
// Broadcast theme change to iframes
const iframes = document.getElementsByTagName('iframe');
Array.from(iframes).forEach((iframe) => {
iframe.contentWindow.postMessage(
{
type: THEME_CONST.events.BROADCAST,
theme: theme,
},
'*'
);
});
return theme;
};
/**
* Reads the current theme from <html> attributes and sets the corresponding theme.
*/
const updateUserPreferredTheme = () => {
let theme = THEME_CONST.theme.LIGHT;
const docElementAttrs = document.documentElement.attributes;
const definitionAttr = docElementAttrs[THEME_CONST.definitionAttr];
const matchingAttr = docElementAttrs[THEME_CONST.matchingAttr];
if (matchingAttr) {
theme = THEME_CONST.theme.MATCHING;
} else if (definitionAttr) {
// colorMode only exists when theme is not MATCHING
const colorMode = docElementAttrs[THEME_CONST.colorModeAttr].value;
const definitionValue = definitionAttr.value;
if (colorMode === THEME_CONST.colorMode.DARK.colorMode) {
theme = THEME_CONST.theme.DARK;
}
}
return setBitbucketTheme(theme);
};
/**
* Verifies if the module had initialize, if not it will call updateUserPreferredTheme,
* it will return the current theme.
*
* @returns {string} - current theme
*/
const getUserPreferredTheme = () => {
return currentTheme || updateUserPreferredTheme();
};
/**
* Sets a new theme if the theme passed inside data is valid, data is used to simulate
* a REST call in case this functionality is moved to an API.
*
* We use a promise for 'set' to simulate a service call that will be implemented in
* later releases.
*
* Currently it will store the selected theme in client storage.
*
* @returns {Promise} - a promise that resolves with the current theme
*/
const setUserPreferredTheme = (data) => {
if (!data || !THEME_CONST.theme[data.theme]) {
data = {
theme: THEME_CONST.defaultTheme,
};
}
return new Promise((resolve) => {
setThemeToAPI(data.theme);
setBitbucketTheme(data.theme);
resolve(data.theme);
});
};
/**
* Initialize theme module and adds media listeners.
*/
const init = () => {
updateUserPreferredTheme();
Object.values(THEME_CONST.mediaListeners).forEach((mediaListeners) => {
window.matchMedia(mediaListeners).addEventListener('change', updateUserPreferredTheme);
});
// For plugin developers and dark theme quality testing we add a web.colors URL param
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has(THEME_CONST.coverage.urlParam)) {
window.AJS.DesignTokens.setTestingThemeColor(
urlParams.get(THEME_CONST.coverage.urlParam) || THEME_CONST.coverage.default
);
window.AJS.DesignTokens.enableTestingTheme();
}
};
define('@bitbucket/internal/feature/theme', Object.freeze({
DEFINITION_ATTRIBUTE: THEME_CONST.definitionAttr,
COLOR_MODE_ATTRIBUTE: THEME_CONST.colorModeAttr,
THEME_EVENTS: THEME_CONST.events,
THEMES: THEME_CONST.theme,
DEFAULT_THEME: THEME_CONST.defaultTheme,
getUserPreferredTheme,
setUserPreferredTheme,
init,
}));