diff --git a/src/components/Settings/ThemeSelector.vue b/src/components/Settings/ThemeSelector.vue index 3ae79195..6624c19c 100644 --- a/src/components/Settings/ThemeSelector.vue +++ b/src/components/Settings/ThemeSelector.vue @@ -8,7 +8,7 @@ :value="$store.getters.theme" class="theme-dropdown" :tabindex="-2" - @input="themeChanged" + @input="themeChangedInUI" /> import CustomThemeMaker from '@/components/Settings/CustomThemeMaker'; -import { - LoadExternalTheme, - ApplyLocalTheme, - ApplyCustomVariables, -} from '@/utils/ThemeHelper'; -import Defaults, { localStorageKeys } from '@/utils/defaults'; import Keys from '@/utils/StoreMutations'; -import ErrorHandler from '@/utils/ErrorHandler'; import IconPalette from '@/assets/interface-icons/config-color-palette.svg'; +import ThemingMixin from '@/mixins/ThemingMixin'; export default { name: 'ThemeSelector', + mixins: [ThemingMixin], props: { hidePallete: Boolean, }, @@ -47,101 +42,16 @@ export default { CustomThemeMaker, IconPalette, }, - watch: { - /* When theme in VueX store changes, then update theme */ - themeFromStore(newTheme) { - this.selectedTheme = newTheme; - this.updateTheme(newTheme); - }, - }, data() { return { - selectedTheme: '', themeConfiguratorOpen: false, // Control the opening of theme config popup - themeHelper: new LoadExternalTheme(), - ApplyLocalTheme, - ApplyCustomVariables, }; }, - computed: { - /* Get appConfig from store */ - appConfig() { - return this.$store.getters.appConfig; - }, - /* Get users theme from store */ - themeFromStore() { - return this.$store.getters.theme; - }, - /* Combines all theme names (builtin and user defined) together */ - themeNames: function themeNames() { - const externalThemeNames = Object.keys(this.externalThemes); - const specialThemes = ['custom']; - return [...this.extraThemeNames, ...externalThemeNames, - ...Defaults.builtInThemes, ...specialThemes]; - }, - extraThemeNames() { - const userThemes = this.appConfig.cssThemes || []; - if (typeof userThemes === 'string') return [userThemes]; - return userThemes; - }, - /* Returns an array of links to external CSS from the Config */ - externalThemes() { - const availibleThemes = {}; - if (this.appConfig && this.appConfig.externalStyleSheet) { - const externals = this.appConfig.externalStyleSheet; - if (Array.isArray(externals)) { - externals.forEach((ext, i) => { - availibleThemes[`External Stylesheet ${i + 1}`] = ext; - }); - } else if (typeof externals === 'string') { - availibleThemes['External Stylesheet'] = this.appConfig.externalStyleSheet; - } else { - ErrorHandler('External stylesheets must be of type string or string[]'); - } - } - // availibleThemes.Default = '#'; - return availibleThemes; - }, - }, + computed: {}, mounted() { - const initialTheme = this.getInitialTheme(); - this.selectedTheme = initialTheme; - // Quicker loading, if the theme is local we can apply it immidiatley - if (this.isThemeLocal(initialTheme)) { - this.updateTheme(initialTheme); - } - - // If it's an external stylesheet, then wait for promise to resolve - if (this.externalThemes && Object.entries(this.externalThemes).length > 0) { - const added = Object.keys(this.externalThemes).map( - name => this.themeHelper.add(name, this.externalThemes[name]), - ); - // Once, added, then apply users initial theme - Promise.all(added).then(() => { - this.updateTheme(initialTheme); - }); - } + this.initializeTheme(); }, methods: { - /* Called when dropdown changed - * Updates store, which will in turn update theme through watcher - */ - themeChanged() { - const pageId = this.$store.state.currentConfigInfo?.pageId || null; - this.$store.commit(Keys.SET_THEME, { theme: this.selectedTheme, pageId }); - this.updateTheme(this.selectedTheme); - }, - /* Returns the initial theme */ - getInitialTheme() { - const localTheme = localStorage[localStorageKeys.THEME]; - if (localTheme && localTheme !== 'undefined') return localTheme; - return this.appConfig.theme || Defaults.theme; - }, - /* Determines if a given theme is local / not a custom user stylesheet */ - isThemeLocal(themeToCheck) { - const localThemes = [...Defaults.builtInThemes, ...this.extraThemeNames]; - return localThemes.includes(themeToCheck); - }, /* Opens the theme color configurator popup */ openThemeConfigurator() { this.$store.commit(Keys.SET_MODAL_OPEN, true); @@ -154,24 +64,6 @@ export default { this.themeConfiguratorOpen = false; } }, - /* Updates theme. Checks if the new theme is local or external, - and calls appropirate updating function. Updates local storage */ - updateTheme(newTheme) { - if (newTheme === 'Default') { - this.resetToDefault(); - this.themeHelper.theme = 'Default'; - } else if (this.isThemeLocal(newTheme)) { - this.ApplyLocalTheme(newTheme); - } else { - this.themeHelper.theme = newTheme; - } - this.ApplyCustomVariables(newTheme); - // localStorage.setItem(localStorageKeys.THEME, newTheme); - }, - /* Removes any applied themes */ - resetToDefault() { - document.getElementsByTagName('html')[0].removeAttribute('data-theme'); - }, }, }; diff --git a/src/mixins/HomeMixin.js b/src/mixins/HomeMixin.js index ea855c6f..63f15243 100644 --- a/src/mixins/HomeMixin.js +++ b/src/mixins/HomeMixin.js @@ -6,7 +6,6 @@ import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults'; import Keys from '@/utils/StoreMutations'; import { searchTiles } from '@/utils/Search'; import { checkItemVisibility } from '@/utils/CheckItemVisibility'; -import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper'; const HomeMixin = { props: { @@ -40,16 +39,18 @@ const HomeMixin = { }, watch: { async $route() { - await this.getConfigForRoute(); - this.setTheme(); + this.loadUpConfig(); }, }, async created() { - // console.log(this.$router.currentRoute.path); - const subPage = this.determineConfigFile(); - await this.$store.dispatch(Keys.INITIALIZE_CONFIG, subPage); + this.loadUpConfig(); }, methods: { + /* When page loaded / sub-page changed, initiate config fetch */ + async loadUpConfig() { + const subPage = this.determineConfigFile(); + await this.$store.dispatch(Keys.INITIALIZE_CONFIG, subPage); + }, /* Based on the current route, get which config to display, null will use default */ determineConfigFile() { const pagePath = this.$router.currentRoute.path; @@ -75,9 +76,9 @@ const HomeMixin = { } }, setTheme() { - const theme = this.getSubPageTheme() || GetTheme(); - ApplyLocalTheme(theme); - ApplyCustomVariables(theme); + // const theme = this.getSubPageTheme() || GetTheme(); + // ApplyLocalTheme(theme); + // ApplyCustomVariables(theme); }, updateModalVisibility(modalState) { this.$store.commit('SET_MODAL_OPEN', modalState); diff --git a/src/mixins/ThemingMixin.js b/src/mixins/ThemingMixin.js index 205a6948..6eddb131 100644 --- a/src/mixins/ThemingMixin.js +++ b/src/mixins/ThemingMixin.js @@ -1,24 +1,26 @@ -// import { -// LoadExternalTheme, -// ApplyLocalTheme, -// ApplyCustomVariables, -// } from '@/utils/ThemeHelper'; -import { builtInThemes, localStorageKeys, mainCssVars } from '@/utils/defaults'; +/** + * This mixin can be extended by any component or view which needs to manage themes + * It handles fetching and applying themes from the store, updating themes, + * applying custom CSS variables and loading external stylesheets. + * */ + import Keys from '@/utils/StoreMutations'; import ErrorHandler from '@/utils/ErrorHandler'; +import { builtInThemes, localStorageKeys, mainCssVars } from '@/utils/defaults'; const ThemingMixin = { data: () => ({ - selectedTheme: '', - // themeHelper: new LoadExternalTheme(), + selectedTheme: '', // Used only to bind current them to theme dropdown }), computed: { + /* This is the theme from the central store. When it changes, the UI will update */ themeFromStore() { return this.$store.getters.theme; }, appConfig() { return this.$store.getters.appConfig; }, + /* Any extra user-defined themes, to add to dropdown */ extraThemeNames() { const userThemes = this.appConfig?.cssThemes || []; if (typeof userThemes === 'string') return [userThemes]; @@ -26,22 +28,22 @@ const ThemingMixin = { }, /* If user specified external stylesheet(s), format and return */ externalThemes() { - const availibleThemes = {}; + const availableThemes = {}; if (this.appConfig?.externalStyleSheet) { const externals = this.appConfig.externalStyleSheet; if (Array.isArray(externals)) { externals.forEach((ext, i) => { - availibleThemes[`External Stylesheet ${i + 1}`] = ext; + availableThemes[`External Stylesheet ${i + 1}`] = ext; }); } else if (typeof externals === 'string') { - availibleThemes['External Stylesheet'] = this.appConfig.externalStyleSheet; + availableThemes['External Stylesheet'] = this.appConfig.externalStyleSheet; } else { ErrorHandler('External stylesheets must be of type string or string[]'); } } - return availibleThemes; + return availableThemes; }, - /* Combines all theme names (builtin and user defined) together */ + /* Combines all theme names for dropdown (built-in, user-defined and stylesheets) */ themeNames() { const externalThemeNames = Object.keys(this.externalThemes); return [...this.extraThemeNames, ...externalThemeNames, ...builtInThemes]; @@ -50,6 +52,7 @@ const ThemingMixin = { watch: { /* When theme in VueX store changes, then update theme */ themeFromStore(newTheme) { + this.resetToDefault(); this.selectedTheme = newTheme; this.updateTheme(newTheme); }, @@ -58,9 +61,9 @@ const ThemingMixin = { /* Called when user changes theme through the UI * Updates store, which will in turn update theme through watcher */ - themeChanged() { - this.$store.commit(Keys.SET_THEME, this.selectedTheme); - this.updateTheme(this.selectedTheme); + themeChangedInUI() { + this.$store.commit(Keys.SET_THEME, this.selectedTheme); // Update store + this.updateTheme(this.selectedTheme); // Apply theme to UI }, /** * Gets any custom styles the user has applied, wither from local storage, or from the config @@ -87,26 +90,40 @@ const ThemingMixin = { if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme'); htmlTag.setAttribute('data-theme', newTheme); }, + /* If using an external stylesheet, load it in */ + applyRemoteTheme(href) { + this.resetToDefault(); + const element = document.createElement('link'); + element.setAttribute('rel', 'stylesheet'); + element.setAttribute('type', 'text/css'); + element.setAttribute('id', 'user-defined-stylesheet'); + element.setAttribute('href', href); + document.getElementsByTagName('head')[0].appendChild(element); + }, /* Determines if a given theme is local / not a custom user stylesheet */ isThemeLocal(themeToCheck) { const localThemes = [...builtInThemes, ...this.extraThemeNames]; return localThemes.includes(themeToCheck); }, /* Updates theme. Checks if the new theme is local or external, - and calls appropirate updating function. Updates local storage */ + and calls appropriate updating function. Updates local storage */ updateTheme(newTheme) { - // this.themeHelper.theme = newTheme; if (newTheme.toLowerCase() === 'default') { this.resetToDefault(); } else if (this.isThemeLocal(newTheme)) { this.applyLocalTheme(newTheme); + } else if (this.externalThemes[newTheme]) { + this.applyRemoteTheme(this.externalThemes[newTheme]); } this.applyCustomVariables(newTheme); }, - /* Removes any applied themes */ + /* Removes any applied themes, and deletes any externally loaded stylesheets */ resetToDefault() { + const externalStyles = document.getElementById('user-defined-stylesheet'); + if (externalStyles) document.getElementsByTagName('head')[0].removeChild(externalStyles); document.getElementsByTagName('html')[0].removeAttribute('data-theme'); }, + /* Call within mounted hook within a page to apply the correct theme */ initializeTheme() { const initialTheme = this.themeFromStore; this.selectedTheme = initialTheme; @@ -115,13 +132,7 @@ const ThemingMixin = { if (this.isThemeLocal(initialTheme)) { this.updateTheme(initialTheme); } else if (hasExternal) { - const added = Object.keys(this.externalThemes).map( - name => this.themeHelper.add(name, this.externalThemes[name]), - ); - // Once, added, then apply users initial theme - Promise.all(added).then(() => { - this.updateTheme(initialTheme); - }); + this.applyRemoteTheme(this.externalThemes[initialTheme]); } }, }, diff --git a/src/store.js b/src/store.js index 59c0df14..2f379599 100644 --- a/src/store.js +++ b/src/store.js @@ -349,7 +349,6 @@ const store = new Vuex.Store({ const configContent = yaml.load(response.data); // Certain values must be inherited from root config const theme = configContent?.appConfig?.theme || rootConfig?.appConfig?.theme; - console.log(theme); configContent.appConfig = rootConfig.appConfig; configContent.pages = rootConfig.pages; configContent.appConfig.theme = theme; diff --git a/src/utils/ConfigHelpers.js b/src/utils/ConfigHelpers.js index 474ae442..544fd8e1 100644 --- a/src/utils/ConfigHelpers.js +++ b/src/utils/ConfigHelpers.js @@ -4,7 +4,6 @@ import { languages } from '@/utils/languages'; import { visibleComponents, localStorageKeys, - theme as defaultTheme, language as defaultLanguage, } from '@/utils/defaults'; import ErrorHandler from '@/utils/ErrorHandler'; @@ -26,6 +25,13 @@ export const makePageSlug = (pageName, pageType) => { return `/${pageType}/${formattedName}`; }; +/* Put fetch path for additional configs in correct format */ +export const formatConfigPath = (configPath) => { + if (configPath.includes('http')) return configPath; + if (configPath.substring(0, 1) !== '/') return `/${configPath}`; + return configPath; +}; + /** * Initiates the Accumulator class and generates a complete config object * Self-executing function, returns the full user config as a JSON object @@ -67,27 +73,6 @@ export const componentVisibility = (appConfig) => { }; }; -/** - * Gets the users saved theme, first looks for local storage theme, - * then looks at user's appConfig, and finally checks the defaults - * @returns {string} Name of theme to apply - */ -export const getTheme = () => { - const localTheme = localStorage[localStorageKeys.THEME]; - const appConfigTheme = config.appConfig.theme; - return localTheme || appConfigTheme || defaultTheme; -}; - -/** - * Gets any custom styles the user has applied, wither from local storage, or from the config - * @returns {object} An array of objects, one for each theme, containing kvps for variables - */ -export const getCustomColors = () => { - const localColors = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}'); - const configColors = config.appConfig.customColors || {}; - return Object.assign(configColors, localColors); -}; - /** * Returns a list of items which the user has assigned a hotkey to * So that when the hotkey is pressed, the app/ service can be launched diff --git a/src/utils/StoreMutations.js b/src/utils/StoreMutations.js index c9c1c68d..6ca0001b 100644 --- a/src/utils/StoreMutations.js +++ b/src/utils/StoreMutations.js @@ -5,7 +5,7 @@ const KEY_NAMES = [ 'INITIALIZE_MULTI_PAGE_CONFIG', 'SET_CONFIG', 'SET_ROOT_CONFIG', - 'SET_REMOTE_CONFIG', + 'SET_CONFIG_ID', 'SET_CURRENT_SUB_PAGE', 'SET_MODAL_OPEN', 'SET_LANGUAGE', diff --git a/src/utils/ThemeHelper.js b/src/utils/ThemeHelper.js deleted file mode 100644 index 3b21dd83..00000000 --- a/src/utils/ThemeHelper.js +++ /dev/null @@ -1,72 +0,0 @@ -import ErrorHandler from '@/utils/ErrorHandler'; -import { getTheme, getCustomColors } from '@/utils/ConfigHelpers'; -import { mainCssVars } from '@/utils/defaults'; - -/* Returns users current theme */ -export const GetTheme = () => getTheme(); - -/* Gets user custom color preferences for current theme, and applies to DOM */ -export const ApplyCustomVariables = (theme) => { - mainCssVars.forEach((vName) => { document.documentElement.style.removeProperty(`--${vName}`); }); - const themeColors = getCustomColors()[theme]; - if (themeColors) { - Object.keys(themeColors).forEach((customVar) => { - document.documentElement.style.setProperty(`--${customVar}`, themeColors[customVar]); - }); - } -}; - -/* Sets the theme, by updating data-theme attribute on the html tag */ -export const ApplyLocalTheme = (newTheme) => { - const htmlTag = document.getElementsByTagName('html')[0]; - if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme'); - htmlTag.setAttribute('data-theme', newTheme); -}; - -/** - * A function for pre-loading, and easy switching of external stylesheets - * External CSS is preloaded to avoid FOUC - */ -export const LoadExternalTheme = function th() { - /* Preload selected external theme */ - const preloadTheme = (href) => { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = href; - document.head.appendChild(link); - return new Promise((resolve, reject) => { - link.onload = e => { - const { sheet } = e.target; - sheet.disabled = true; - resolve(sheet); - }; - link.onerror = reject; - }); - }; - - /* Check theme is selected, and it exists */ - const checkTheme = (themes, name) => { - if ((!name) || (name !== 'custom' && !themes[name])) { - ErrorHandler(`Theme: '${name || '[not selected]'}' does not exist.`); - return false; - } - return true; - }; - - /* Disable all but selected theme */ - const selectTheme = (themes, name) => { - if (checkTheme(themes, name)) { - const t = themes; // To avoid ESLint complaining about mutating a param - Object.keys(themes).forEach(n => { t[n].disabled = (n !== name); }); - } - }; - - const themes = {}; - - return { - add(name, href) { return preloadTheme(href).then(s => { themes[name] = s; }); }, - set theme(name) { selectTheme(themes, name); }, - get theme() { return Object.keys(themes).find(n => !themes[n].disabled); }, - }; -}; diff --git a/src/views/Workspace.vue b/src/views/Workspace.vue index 972ad91c..7d876afe 100644 --- a/src/views/Workspace.vue +++ b/src/views/Workspace.vue @@ -19,7 +19,6 @@ import WebContent from '@/components/Workspace/WebContent'; import WidgetView from '@/components/Workspace/WidgetView'; import MultiTaskingWebComtent from '@/components/Workspace/MultiTaskingWebComtent'; import Defaults from '@/utils/defaults'; -import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper'; export default { name: 'Workspace', @@ -27,9 +26,6 @@ export default { data: () => ({ url: '', widgets: null, - GetTheme, - ApplyLocalTheme, - ApplyCustomVariables, }), computed: { sections() {