Restructure store data flow (see comments)
- Moves all `AddonManager` and `Toasts` usage out of `BdWebApi` to prevent circular dependencies. -Replaces `BdWebApi.installAddon` with `getAddonContents` and an `AddonManager.installAddon` - Move store-specific strings into `Strings.Store` (rather than `Strings.Addons`). - TabBar component that's actually accessible. Replaces the previous inaccessible one in the store and `AddonErrorModal`. - StoreCard is now Pure. - InstallationModal is now consistent with StoreCard's props terminolgy. - search.js => searchbar.js for consistent naming. - Better error handling for everything. - lint
This commit is contained in:
parent
63ef11614a
commit
402d25af32
|
@ -259,19 +259,7 @@
|
|||
"compileError": "Could not be compiled. See console for details.",
|
||||
"wasUnloaded": "{{name}} was unloaded.",
|
||||
"blankSlateHeader": "You don't have any {{type}}s!",
|
||||
"blankSlateMessage": "Download some from the store to get started.",
|
||||
"installConfirmation": "Do you want to add this to your installed {{type}}s?",
|
||||
"new": "New",
|
||||
"likes": "Likes",
|
||||
"downloads": "Downloads",
|
||||
"likesAmount": "{{amount}} likes",
|
||||
"downloadsAmount": "{{amount}} downloads",
|
||||
"uploadDate": "Uploaded on {{date}}",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"connectError": "Failed to connect to Store: {{error}}",
|
||||
"downloadError": "Failed to download {{type}}: {{error}}",
|
||||
"writeError": "Failed to write {{type}} to disk: {{error}}"
|
||||
"blankSlateMessage": "Download some from the store to get started."
|
||||
},
|
||||
"CustomCSS": {
|
||||
"confirmationText": "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.",
|
||||
|
@ -280,7 +268,22 @@
|
|||
"openNative": "Open in System Editor",
|
||||
"openDetached": "Detach Window",
|
||||
"settings": "Editor Settings",
|
||||
"editorTitle": "Custom CSS Editor"
|
||||
"editorTitle": "Custom CSS Editor",
|
||||
"writeError": "Failed to write {{type}} to disk."
|
||||
},
|
||||
"Store": {
|
||||
"connectionError": "Failed to connect to Store.",
|
||||
"connectionErrorMessage": "There was an error connecting to the store, it's possible our website/api is down. Would you like to retry?",
|
||||
"downloadError": "Failed to download {{type}}.",
|
||||
"installConfirmation": "Do you want to add this to your installed {{type}}s?",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"new": "New",
|
||||
"likes": "Likes",
|
||||
"likesAmount": "{{amount}} likes",
|
||||
"downloads": "Downloads",
|
||||
"downloadsAmount": "{{amount}} downloads",
|
||||
"uploadDate": "Uploaded on {{date}}"
|
||||
},
|
||||
"Emotes": {
|
||||
"loading": "Loading emotes in the background do not reload.",
|
||||
|
@ -318,6 +321,7 @@
|
|||
"cancel": "Cancel",
|
||||
"nevermind": "Nevermind",
|
||||
"close": "Close",
|
||||
"retry": "Retry",
|
||||
"name": "Name",
|
||||
"message": "Message",
|
||||
"error": "Error",
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import {React, WebpackModules, BdWebApi, Strings} from "modules";
|
||||
import {Web} from "data";
|
||||
|
||||
import Builtin from "../../structs/builtin";
|
||||
import {React, WebpackModules} from "modules";
|
||||
import PluginManager from "../../modules/pluginmanager";
|
||||
import ThemeManager from "../../modules/thememanager";
|
||||
import BdWebApi from "../../modules/bdwebapi";
|
||||
import StoreCard from "../../ui/settings/addonlist/storecard";
|
||||
// import openStoreDetail from "../../ui/settings/addonlist/storedetail";
|
||||
import Modals from "../../ui/modals";
|
||||
import Toasts from "../../ui/toasts";
|
||||
|
||||
import {URL} from "url";
|
||||
|
||||
|
@ -37,7 +37,7 @@ export default new class Store extends Builtin {
|
|||
this.instead(MessageAccessories.prototype, "renderEmbeds", (thisObject, methodArguments, renderEmbeds) => {
|
||||
const embeds = Reflect.apply(renderEmbeds, thisObject, methodArguments);
|
||||
const matchedProtocols = methodArguments[0]?.content.match(PROTOCOL_REGEX)?.map(match => {
|
||||
const url = new URL(match.replace(/\s+/g, ' ').replaceAll(/[<>]+/g, "").trim());
|
||||
const url = new URL(match.replace(/\s+/g, " ").replaceAll(/[<>]+/g, "").trim());
|
||||
|
||||
if (url.hostname === "addon" && url.protocol === BD_PROTOCOL) return url;
|
||||
}).filter(m => m != null);
|
||||
|
@ -104,7 +104,7 @@ export default new class Store extends Builtin {
|
|||
event.preventDefault();
|
||||
window.open((Web.PAGES[addon.type] + (typeof(addonId) === "number" ? "?id=" : "/") + addonId), "_blank");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return link;
|
||||
|
@ -125,34 +125,40 @@ class EmbeddedStoreCard extends React.Component {
|
|||
if (data?.id) this.setState({addon: data});
|
||||
});
|
||||
}
|
||||
|
||||
isInstalled = (filename) => {
|
||||
return this.manager.isLoaded(filename);
|
||||
}
|
||||
|
||||
|
||||
get manager() {
|
||||
return this.state.addon?.type === "theme" ? ThemeManager : PluginManager;
|
||||
}
|
||||
|
||||
async install(id, filename) {
|
||||
await BdWebApi.getAddonContents(id).then(contents => {
|
||||
return this.manager.installAddon(contents, filename);
|
||||
}).catch(err => {
|
||||
Toasts.error(Strings.Store.downloadError.format({type: this.state.addon.type}), err);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {addon} = this.state;
|
||||
const {manager} = this;
|
||||
|
||||
return [
|
||||
addon ? React.createElement(StoreCard, {
|
||||
return addon ? React.createElement(StoreCard, {
|
||||
...addon,
|
||||
key: addon.id,
|
||||
className: "bd-store-card-embedded",
|
||||
thumbnail: Web.ENDPOINTS.thumbnail(addon.thumbnail_url),
|
||||
filename: addon.file_name,
|
||||
releaseDate: new Date(addon.release_date),
|
||||
isInstalled: this.manager.isLoaded(addon.file_name),
|
||||
onInstall: () => Modals.showInstallationModal({
|
||||
...addon,
|
||||
thumbnail: Web.ENDPOINTS.thumbnail(addon.thumbnail_url),
|
||||
folder: manager.addonfolder,
|
||||
installAddon: BdWebApi.installAddon.bind(BdWebApi),
|
||||
reload: addon.type === "theme" ? ThemeManager.reloadTheme.bind(ThemeManager) : PluginManager.reloadPlugin.bind(PluginManager),
|
||||
deleteAddon: manager.deleteAddon.bind(manager),
|
||||
confirmAddonDelete: manager.confirmAddonDelete.bind(manager),
|
||||
isInstalled: this.isInstalled.bind(this),
|
||||
className: "bd-store-card-embedded",
|
||||
// onDetailsView: () => {
|
||||
// openStoreDetail(addon);
|
||||
// }
|
||||
}) : null
|
||||
]
|
||||
filename: addon.file_name,
|
||||
releaseDate: new Date(addon.release_date),
|
||||
onInstall: () => this.install(addon.id, addon.file_name)
|
||||
}),
|
||||
onForceInstall: () => this.install(addon.id, addon.file_name),
|
||||
onDelete: () => this.manager.confirmAddonDelete(addon.file_name),
|
||||
onForceDelete: () => this.manager.deleteAddon(addon.file_name)
|
||||
}) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -267,6 +267,22 @@ export default class AddonManager {
|
|||
else this.enableAddon(id);
|
||||
}
|
||||
|
||||
installAddon(text, filename, shouldToast = true) {
|
||||
const enable = (id) => {
|
||||
const installation = this.getAddon(id);
|
||||
this.enableAddon(installation);
|
||||
Events.off(`${this.prefix}-loaded`, enable);
|
||||
};
|
||||
fs.writeFileSync(path.resolve(this.addonFolder, filename), text, (error) => {
|
||||
if (error) {
|
||||
Logger.stacktrace(this.name, Strings.Addons.writeError.format({type: this.prefix}), error);
|
||||
if (shouldToast) Toasts.error(Strings.Addons.writeError.format({type: this.prefix}));
|
||||
}
|
||||
});
|
||||
if (!Settings.get("settings", "addons", "autoReload")) this.reloadAddon(filename);
|
||||
if (Settings.get("settings", "addons", "autoEnable")) Events.on(`${this.prefix}-loaded`, enable);
|
||||
}
|
||||
|
||||
loadNewAddons() {
|
||||
const files = fs.readdirSync(this.addonFolder);
|
||||
const removed = this.addonList.filter(t => !files.includes(t.filename)).map(c => c.id);
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import {Web} from "data";
|
||||
|
||||
import Logger from "common/Logger";
|
||||
import Utilities from "./utilities";
|
||||
import Settings from "./settingsmanager";
|
||||
import ThemeManager from "./thememanager";
|
||||
import PluginManager from "./pluginmanager";
|
||||
import Strings from "./strings";
|
||||
import Events from "./emitter";
|
||||
import Logger from "common/logger";
|
||||
|
||||
import Toasts from "../ui/toasts";
|
||||
|
||||
import https from "https";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const API_CACHE = {plugins: [], themes: [], addon: []};
|
||||
// const README_CACHE = {plugins: {}, themes: {}};
|
||||
|
@ -25,52 +17,15 @@ export default new class BdWebApi {
|
|||
get pages() {return Web.PAGES;}
|
||||
get tags() {return Web.TAGS;}
|
||||
|
||||
/**
|
||||
* Fetches an addon by ID and adds writes it to it's respective folder. Enables the addon if the setting is on.
|
||||
* @param {number} id - The ID of the addon to fetch.
|
||||
* @param {string} filename - The name of the file that the addon will be written to.
|
||||
* @param {"theme" | "plugin"} type - The type of the addon (theme or plugin).
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
installAddon(id, filename, type) {
|
||||
const manager = type === "theme" ? ThemeManager : PluginManager;
|
||||
const enable = installationId => {
|
||||
const installation = manager.getAddon(installationId);
|
||||
|
||||
if (manager.enableAddon && installation.filename === filename) manager.enableAddon(installation);
|
||||
Events.off(`${type}-loaded`, enable);
|
||||
};
|
||||
|
||||
return new Promise(resolve => {
|
||||
https.get(Web.ENDPOINTS.download(id), response => {
|
||||
const chunks = [];
|
||||
response.on("data", chunk => chunks.push(chunk));
|
||||
response.on("end", () => {
|
||||
const data = chunks.join("");
|
||||
fs.writeFileSync(path.resolve(manager.addonFolder, filename), data, error => {
|
||||
if (error) Toasts.show(Strings.Addons.writeError.format({type, error}), {type: "error"});
|
||||
});
|
||||
if (!Settings.get("settings", "addons", "autoReload")) type === "theme" ? ThemeManager.reloadTheme(filename) : PluginManager.reloadPlugin(filename);
|
||||
if (Settings.get("settings", "addons", "autoEnable")) Events.on(`${type}-loaded`, enable);
|
||||
resolve(data);
|
||||
});
|
||||
response.on("error", (error) => {
|
||||
Logger.error("Addon Store", Strings.Addons.downloadError.format({type, error}));
|
||||
Toasts.show(Strings.Addons.downloadError.format({type, error}), {type: "error"});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of all addons from the site.
|
||||
* @param {"themes" | "plugins"} type - The type of the addon (theme or plugin).
|
||||
* @returns {Promise<Array<Object>>}
|
||||
*/
|
||||
getAddons(type) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (API_CACHE[type].length) resolve(API_CACHE[type]);
|
||||
https.get(Web.ENDPOINTS.store(type), res => {
|
||||
https.get(Web.ENDPOINTS.store(type), (res) => {
|
||||
const chunks = [];
|
||||
res.on("data", chunk => chunks.push(chunk));
|
||||
|
||||
|
@ -85,8 +40,8 @@ export default new class BdWebApi {
|
|||
});
|
||||
|
||||
res.on("error", (error) => {
|
||||
Logger.error("Addon Store", Strings.Addons.connectError.format({error}));
|
||||
Toasts.show(Strings.Addons.connectError.format({error}), {type: "error"});
|
||||
Logger.stacktrace("BdWebApi", Strings.Store.connectionError, error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -98,11 +53,11 @@ export default new class BdWebApi {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
getAddon(addon) {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cacheMatch = API_CACHE.addon.find(a => a[typeof addon === "number" ? "id" : "name"] === addon);
|
||||
if (cacheMatch) resolve(cacheMatch);
|
||||
|
||||
https.get(Web.ENDPOINTS.addon(addon), res => {
|
||||
https.get(Web.ENDPOINTS.addon(addon), (res) => {
|
||||
const chunks = [];
|
||||
res.on("data", chunk => chunks.push(chunk));
|
||||
|
||||
|
@ -117,8 +72,32 @@ export default new class BdWebApi {
|
|||
});
|
||||
|
||||
res.on("error", (error) => {
|
||||
Logger.error("Addon Store", Strings.Addons.connectError.format({error}));
|
||||
Toasts.show(Strings.Addons.connectError.format({error}), {type: "error"});
|
||||
Logger.stacktrace("BdWebApi", Strings.Store.connectionError, error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and return's an addon's raw text content.
|
||||
* @param {number} id - The ID of the addon to fetch.
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
getAddonContents(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(Web.ENDPOINTS.download(id), (res) => {
|
||||
const chunks = [];
|
||||
res.on("data", chunk => chunks.push(chunk));
|
||||
|
||||
res.on("end", () => {
|
||||
const data = chunks.join("");
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
res.on("error", (error) => {
|
||||
Logger.stacktrace("BdWebApi", Strings.Store.connectionError, error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,4 +17,5 @@ export {default as LocaleManager} from "./localemanager";
|
|||
export {default as Strings} from "./strings";
|
||||
export {default as IPC} from "./ipc";
|
||||
export {default as Logger} from "common/logger";
|
||||
export {default as DiscordClasses} from "./discordclasses";
|
||||
export {default as DiscordClasses} from "./discordclasses";
|
||||
export {default as BdWebApi} from "./bdwebapi";
|
|
@ -53,7 +53,7 @@ export default new class PluginManager extends AddonManager {
|
|||
deleteAddon: this.deleteAddon.bind(this),
|
||||
confirmAddonDelete: this.confirmAddonDelete.bind(this),
|
||||
isLoaded: this.isLoaded.bind(this),
|
||||
prefix: this.prefix
|
||||
installAddon: this.installAddon.bind(this)
|
||||
})});
|
||||
return errors;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ export default new class ThemeManager extends AddonManager {
|
|||
deleteAddon: this.deleteAddon.bind(this),
|
||||
confirmAddonDelete: this.confirmAddonDelete.bind(this),
|
||||
isLoaded: this.isLoaded.bind(this),
|
||||
prefix: this.prefix
|
||||
installAddon: this.installAddon.bind(this)
|
||||
})});
|
||||
return errors;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
.bd-tab-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bd-tab-item {
|
||||
border: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
@ -18,6 +22,7 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
color: var(--interactive-normal);
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
|
@ -46,3 +51,13 @@
|
|||
.bd-tab-item:active {
|
||||
background-color: var(--background-modifier-active);
|
||||
}
|
||||
|
||||
.bd-tab-item.disabled {
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
color: var(--interactive-muted);
|
||||
}
|
||||
|
||||
.bd-tab-item:focus-visible {
|
||||
box-shadow: 0 0 0 4px var(--focus-primary);
|
||||
}
|
|
@ -2,6 +2,7 @@ import {React, Strings, WebpackModules, DiscordClasses, Utilities} from "modules
|
|||
import Extension from "./icons/extension";
|
||||
import ThemeIcon from "./icons/theme";
|
||||
import Divider from "./divider";
|
||||
import TabBar from "./settings/components/tabbar";
|
||||
|
||||
const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules;
|
||||
|
||||
|
@ -104,9 +105,7 @@ export default class AddonErrorModal extends React.Component {
|
|||
return <>
|
||||
<div className={`bd-error-modal-header ${DiscordClasses.Modal.header} ${DiscordClasses.Modal.separator}`}>
|
||||
<h4 className={`${DiscordClasses.Titles.defaultColor} ${DiscordClasses.Text.size14} ${DiscordClasses.Titles.h4} ${DiscordClasses.Margins.marginBottom8}`}>{Strings.Modals.addonErrors}</h4>
|
||||
<div className="bd-tab-bar">
|
||||
{tabs.map(tab => <div onClick={() => {this.switchToTab(tab.id);}} className={Utilities.joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
|
||||
</div>
|
||||
<TabBar items={tabs.map(({id, name}) => ({value: id, name}))} value={selectedTab.id} onChange={value => this.switchToTab(value)} />
|
||||
</div>
|
||||
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
|
||||
<div className="bd-addon-errors">
|
||||
|
|
|
@ -18,6 +18,7 @@ export default class InstallationModal extends React.Component {
|
|||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.state = {
|
||||
isInstalling: false
|
||||
};
|
||||
|
@ -25,24 +26,23 @@ export default class InstallationModal extends React.Component {
|
|||
|
||||
async install(id, filename) {
|
||||
this.setState({isInstalling: true});
|
||||
await this.props.installAddon(id, filename, this.props.type);
|
||||
await this.props.onInstall?.(id, filename, this.props.type);
|
||||
this.setState({isInstalling: false});
|
||||
if (typeof this.props.onInstall === "function") this.props.onInstall();
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
onKeyDown = event => {
|
||||
onKeyDown(event) {
|
||||
const {key} = event;
|
||||
|
||||
if (key === "Escape" || key === "Enter" || key === " ") event.stopPropagation();
|
||||
if (key === "Escape") this.props.onClose();
|
||||
if (key === "Enter" || key === " ") {
|
||||
this.install(this.props.id, this.props.file_name);
|
||||
this.install(this.props.id, this.props.filename);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {name, id, description, author, release_date, type, version, file_name} = this.props;
|
||||
const {name, id, description, author, releaseDate, type, version, filename} = this.props;
|
||||
|
||||
return <ModalRoot {...this.props} onKeyDown={this.onKeyDown} size="small" className="bd-installation-modal">
|
||||
<ModalHeader className="bd-installation-header">
|
||||
|
@ -55,7 +55,7 @@ export default class InstallationModal extends React.Component {
|
|||
<ModalContent className="bd-installation-content">
|
||||
<h5 className={Utilities.joinClassNames("bd-installation-name", DiscordClasses.Text.size16, DiscordClasses.Text.colorHeaderPrimary)}>{name}</h5>
|
||||
<div className={Utilities.joinClassNames(DiscordClasses.Text.size14, DiscordClasses.Text.colorHeaderSecondary)}>
|
||||
{Strings.Addons.installConfirmation.format({type})}
|
||||
{Strings.Store.installConfirmation.format({type})}
|
||||
</div>
|
||||
<ul className="bd-installation-info">
|
||||
<InfoItem icon={<Description aria-label={Strings.Addons.description} />} id="bd-info-description" label={Strings.Addons.description}>
|
||||
|
@ -66,12 +66,12 @@ export default class InstallationModal extends React.Component {
|
|||
{version}
|
||||
</InfoItem>
|
||||
<div className="bd-info-divider" role="separator"></div>
|
||||
<InfoItem icon={<Clock aria-label={Strings.Addons.uploadDate.format({date: new Date(release_date).toLocaleString()})} />} id="bd-info-upload-date" label={Strings.Addons.uploadDate.format({date: new Date(release_date).toLocaleString()})}>
|
||||
{Strings.Addons.uploadDate.format({date: new Date(release_date).toLocaleString()})}
|
||||
<InfoItem icon={<Clock aria-label={Strings.Store.uploadDate.format({date: releaseDate.toLocaleString()})} />} id="bd-info-upload-date" label={Strings.Store.uploadDate.format({date: releaseDate.toLocaleString()})}>
|
||||
{Strings.Store.uploadDate.format({date: releaseDate.toLocaleString()})}
|
||||
</InfoItem>
|
||||
<div className="bd-info-divider" role="separator"></div>
|
||||
<InfoItem icon={<Github aria-label={Strings.Addons.source} />} id="bd-info-source" label={Strings.Addons.source}>
|
||||
<Anchor href={Web.ENDPOINTS.githubRedirect(id)} target="_blank" rel="noreferrer noopener">{file_name}</Anchor>
|
||||
<Anchor href={Web.ENDPOINTS.githubRedirect(id)} target="_blank" rel="noreferrer noopener">{filename}</Anchor>
|
||||
</InfoItem>
|
||||
<div className="bd-info-divider" role="separator"></div>
|
||||
<InfoItem icon={<Author aria-label={Strings.Addons.author} />} id="bd-info-author" label={Strings.Addons.uploaded}>
|
||||
|
@ -80,7 +80,7 @@ export default class InstallationModal extends React.Component {
|
|||
</ul>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button onClick={() => this.install(id, file_name)} color={Button.Colors.GREEN} disabled={this.state.isInstalling}>
|
||||
<Button onClick={() => this.install(id, filename)} color={Button.Colors.GREEN} disabled={this.state.isInstalling}>
|
||||
{this.state.isInstalling ? <Spinner type={Spinner.Type.PULSING_ELLIPSIS} /> : (Strings.Modals.install ?? "Install")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
|
|
@ -110,11 +110,11 @@ export default class Modals {
|
|||
className: "bd-image-modal"
|
||||
}), React.createElement(ImageModal, Object.assign({
|
||||
className: "bd-image-modal-image",
|
||||
src: src,
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
placeholder: src,
|
||||
original: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onClickUntrusted: link => link.openHref(),
|
||||
renderLinkComponent: () => React.createElement(MaskedLink, props)
|
||||
}, props)));
|
||||
|
|
|
@ -4,7 +4,7 @@ import SettingsTitle from "../settings/title";
|
|||
import ServerCard from "./card";
|
||||
import EmptyResults from "../blankslates/noresults";
|
||||
import Connection from "../../structs/psconnection";
|
||||
import SearchBar from "../settings/components/search";
|
||||
import SearchBar from "../settings/components/searchbar";
|
||||
import Previous from "../icons/previous";
|
||||
import Next from "../icons/next";
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ import {Web} from "data";
|
|||
import {shell} from "electron";
|
||||
|
||||
import Dropdown from "../components/dropdown";
|
||||
import SearchBar from "../components/search";
|
||||
import SearchBar from "../components/searchbar";
|
||||
import TabBar from "../components/tabbar";
|
||||
import Divider from "../../divider";
|
||||
import SettingsTitle from "../title";
|
||||
import Reload from "../../icons/reload";
|
||||
|
@ -36,8 +37,8 @@ const CONTROLS = {
|
|||
store: {
|
||||
sortOptions: [
|
||||
{get label() {return Strings.Addons.name;}, value: "name"},
|
||||
{get label() {return Strings.Addons.likes;}, value: "likes"},
|
||||
{get label() {return Strings.Addons.downloads;}, value: "downloads"},
|
||||
{get label() {return Strings.Store.Likes;}, value: "likes"},
|
||||
{get label() {return Strings.Store.downloads;}, value: "downloads"},
|
||||
{get label() {return Strings.Addons.added;}, value: "release_date"}
|
||||
],
|
||||
directions: [
|
||||
|
@ -86,11 +87,11 @@ const PAGES = {
|
|||
};
|
||||
|
||||
export default class AddonList extends React.Component {
|
||||
events = [`${this.props.prefix}-loaded`, `${this.props.prefix}-unloaded`];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.update = this.update.bind(this);
|
||||
this.events = [`${this.props.type}-loaded`, `${this.props.type}-unloaded`];
|
||||
this.state = {
|
||||
query: "",
|
||||
selectedTag: "all",
|
||||
|
@ -151,7 +152,7 @@ export default class AddonList extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
update = () => {this.forceUpdate();}
|
||||
update() {this.forceUpdate();}
|
||||
|
||||
getControlState(control, defaultValue) {
|
||||
const {type} = this.props;
|
||||
|
@ -176,16 +177,6 @@ export default class AddonList extends React.Component {
|
|||
DataStore.setBDData(id, controls);
|
||||
}
|
||||
|
||||
makeTab({label, selected, onSelect = () => {}}) {
|
||||
return <div
|
||||
className={Utilities.joinClassNames("bd-tab-item", {selected})}
|
||||
role="tab"
|
||||
aria-disabled="false"
|
||||
aria-selected={selected}
|
||||
onClick={onSelect}
|
||||
>{label}</div>;
|
||||
}
|
||||
|
||||
reload() {
|
||||
if (this.props.refreshList) this.props.refreshList();
|
||||
this.forceUpdate();
|
||||
|
@ -221,16 +212,16 @@ export default class AddonList extends React.Component {
|
|||
return <React.Fragment>
|
||||
<div className="bd-addon-list-title">
|
||||
{storeEnabled ?
|
||||
<div className="bd-tab-bar">
|
||||
{Object.entries(PAGES).map(([id, props]) => {
|
||||
return this.makeTab({
|
||||
label: props.label,
|
||||
selected: this.state.page === id,
|
||||
onSelect: () => this.setState({page: id})
|
||||
});
|
||||
})}
|
||||
<TabBar
|
||||
value={this.state.page}
|
||||
onChange={value => this.setState({ page: value })}
|
||||
items={Object.entries(PAGES).map(([id, props]) => ({
|
||||
name: props.label,
|
||||
value: id
|
||||
}))}
|
||||
>
|
||||
{showReloadIcon && <Reload className="bd-reload" onClick={this.reload.bind(this)} />}
|
||||
</div>
|
||||
</TabBar>
|
||||
: <SettingsTitle key="title" text={this.props.title} otherChildren={showReloadIcon && <Reload className="bd-reload" onClick={this.reload} />} />
|
||||
}
|
||||
<div className="bd-addon-list-filters">
|
||||
|
@ -295,6 +286,7 @@ export default class AddonList extends React.Component {
|
|||
refreshList={this.props.refreshList}
|
||||
isLoaded={this.props.isLoaded}
|
||||
deleteAddon={this.props.deleteAddon}
|
||||
installAddon={this.props.installAddon}
|
||||
editAddon={this.editAddon}
|
||||
confirmAddonDelete={this.props.confirmAddonDelete}
|
||||
view={this.viewStyle}
|
||||
|
|
|
@ -9,17 +9,23 @@ import NoResults from "../../blankslates/noresults";
|
|||
import EmptyImage from "../../blankslates/emptyimage";
|
||||
|
||||
export default class InstalledPage extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.update = this.update.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Events.on(`${this.props.prefix}-loaded`, this.update);
|
||||
Events.on(`${this.props.prefix}-unloaded`, this.update);
|
||||
Events.on(`${this.props.type}-loaded`, this.update);
|
||||
Events.on(`${this.props.type}-unloaded`, this.update);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Events.off(`${this.props.prefix}-loaded`, this.update);
|
||||
Events.off(`${this.props.prefix}-unloaded`, this.update);
|
||||
Events.off(`${this.props.type}-loaded`, this.update);
|
||||
Events.off(`${this.props.type}-unloaded`, this.update);
|
||||
}
|
||||
|
||||
update = () => {
|
||||
update() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import {React, Strings, Utilities, WebpackModules, DiscordClasses} from "modules";
|
||||
import {React, Strings, Utilities, WebpackModules, DiscordClasses, BdWebApi} from "modules";
|
||||
import {Web} from "data";
|
||||
|
||||
import Next from "../../icons/next";
|
||||
import Previous from "../../icons/previous";
|
||||
import NoResults from "../../blankslates/noresults";
|
||||
import StoreCard from "./storecard";
|
||||
// import openStoreDetail from "./storedetail";
|
||||
|
||||
import BdWebApi from "../../../modules/bdwebapi";
|
||||
import Modals from "../../modals";
|
||||
import Toasts from "../../toasts";
|
||||
|
||||
const Button = WebpackModules.getByProps("DropdownSizes");
|
||||
const Spinner = WebpackModules.getByDisplayName("Spinner");
|
||||
|
@ -16,6 +15,8 @@ export default class StorePage extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.matchAddon = this.matchAddon.bind(this);
|
||||
this.filterTags = this.filterTags.bind(this);
|
||||
this.state = {
|
||||
isLoaded: false,
|
||||
addons: null,
|
||||
|
@ -24,22 +25,38 @@ export default class StorePage extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
BdWebApi.getAddons(`${this.props.type}s`).then(data => {
|
||||
this.setState({
|
||||
isLoaded: true,
|
||||
addons: data
|
||||
});
|
||||
}).catch((err) => Modals.showConfirmationModal(Strings.Store.connectionError, Strings.Store.connectionErrorMessage, {
|
||||
cancelText: Strings.Modals.close,
|
||||
confirmText: Strings.Modals.retry,
|
||||
onConfirm: () => this.connect()
|
||||
}));
|
||||
}
|
||||
|
||||
async install(id, filename) {
|
||||
await BdWebApi.getAddonContents(id).then(contents => {
|
||||
return this.props.installAddon(contents, filename);
|
||||
}).catch(err => {
|
||||
Toasts.error(Strings.Store.downloadError.format({ type: this.props.type }), err);
|
||||
});
|
||||
}
|
||||
|
||||
matchAddon = (addon, query) => {
|
||||
matchAddon(addon, query) {
|
||||
let matches = ~addon.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
||||
matches = matches || ~addon.author.display_name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
||||
matches = matches || ~addon.description.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
||||
return matches;
|
||||
}
|
||||
|
||||
filterTags = (addon) => {
|
||||
filterTags(addon) {
|
||||
const matches = this.matchAddon(addon, this.props.query);
|
||||
const tagMatches = this.props.state.selectedTag === "all" || addon.tags.some(tag => tag === this.props.state.selectedTag);
|
||||
return tagMatches && matches;
|
||||
|
@ -68,14 +85,11 @@ export default class StorePage extends React.Component {
|
|||
return Utilities.splitArray(final, 16);
|
||||
}
|
||||
|
||||
isInstalled = (filename) => {
|
||||
return this.props.isLoaded(filename);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.query !== this.latestSearchQuery || this.props.state.selectedTag !== this.latestSelectedTag) this.setState({selectedPage: 0});
|
||||
const containerState = this.props.state;
|
||||
if (this.props.query !== this.latestSearchQuery || containerState.selectedTag !== this.latestSelectedTag) this.setState({selectedPage: 0});
|
||||
this.latestSearchQuery = this.props.query;
|
||||
this.latestSelectedTag = this.props.state.selectedTag;
|
||||
this.latestSelectedTag = containerState.selectedTag;
|
||||
const addons = this.addons;
|
||||
const canGoForward = addons && (addons.length > 1 && this.state.selectedPage < addons.length - 1);
|
||||
const canGoBackward = this.state.selectedPage > 0;
|
||||
|
@ -92,19 +106,26 @@ export default class StorePage extends React.Component {
|
|||
{(this.state.isLoaded && addons?.length && addons[this.state.selectedPage])
|
||||
? <div className={Utilities.joinClassNames("bd-store-addons", this.props.view + "-view")}>
|
||||
{addons[this.state.selectedPage].map(addon => {
|
||||
const thumbnail = Web.ENDPOINTS.thumbnail(addon.thumbnail_url);
|
||||
|
||||
return <StoreCard
|
||||
{...addon}
|
||||
deleteAddon={this.props.deleteAddon}
|
||||
installAddon={BdWebApi.installAddon.bind(this)}
|
||||
thumbnail={Web.ENDPOINTS.thumbnail(addon.thumbnail_url)}
|
||||
reload={this.props.reload}
|
||||
confirmAddonDelete={this.props.confirmAddonDelete}
|
||||
isInstalled={this.isInstalled.bind(this)}
|
||||
selectedTag={this.props.state.selectedTag}
|
||||
folder={this.props.folder}
|
||||
// onDetailsView={() => {
|
||||
// openStoreDetail(addon);
|
||||
// }}
|
||||
key={addon.id}
|
||||
filename={addon.file_name}
|
||||
releaseDate={new Date(addon.release_date)}
|
||||
thumbnail={thumbnail}
|
||||
selectedTag={containerState.selectedTag}
|
||||
isInstalled={this.props.isLoaded(addon.file_name)}
|
||||
onInstall={() => Modals.showInstallationModal({
|
||||
...addon,
|
||||
thumbnail,
|
||||
filename: addon.file_name,
|
||||
releaseDate: new Date(addon.release_date),
|
||||
onInstall: () => this.install(addon.id, addon.file_name)
|
||||
})}
|
||||
onForceInstall={() => this.install(addon.id, addon.file_name)}
|
||||
onDelete={() => this.props.confirmAddonDelete(addon.file_name)}
|
||||
onForceDelete={() => manager.deleteAddon(addon.file_name)}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
|
@ -117,7 +138,13 @@ export default class StorePage extends React.Component {
|
|||
</Button>
|
||||
<div class={`bd-page-buttons ${DiscordClasses.Scrollers.thin}`}>
|
||||
{addons.length
|
||||
? addons.map((_, index) => <div role="button" aria-current="page" tabindex="0" className={Utilities.joinClassNames("bd-page-item bd-page-button", {selected: index === this.state.selectedPage})} onClick={handleSelect(() => index)}>
|
||||
? addons.map((_, index) => <div
|
||||
role="button"
|
||||
aria-current="page"
|
||||
tabIndex="0"
|
||||
className={Utilities.joinClassNames("bd-page-item bd-page-button", {selected: index === this.state.selectedPage})}
|
||||
onClick={handleSelect(() => index)}
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
</div>)
|
||||
: null
|
||||
|
|
|
@ -7,30 +7,22 @@ import Download from "../../icons/download";
|
|||
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
|
||||
const Button = WebpackModules.getByProps("DropdownSizes");
|
||||
|
||||
export default class StoreCard extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isInstalled: props.isInstalled(this.props.file_name)
|
||||
};
|
||||
}
|
||||
|
||||
get monthsAgo() {
|
||||
const current = new Date();
|
||||
const release = new Date(this.props.release_date);
|
||||
let months = (((current.getFullYear() - release.getFullYear()) * 12) - release.getMonth()) + current.getMonth();
|
||||
|
||||
return Math.max(months, 0);
|
||||
}
|
||||
|
||||
export default class StoreCard extends React.PureComponent {
|
||||
abbreviateStat(n) {
|
||||
if (n < 1e3) return n;
|
||||
if (n >= 1e3 && n < 1e6) return +(n / 1e3).toFixed(1) + "K";
|
||||
if (n >= 1e6 && n < 1e9) return +(n / 1e6).toFixed(1) + "M";
|
||||
}
|
||||
|
||||
preview = (event) => {
|
||||
monthsAgo(date) {
|
||||
const current = new Date();
|
||||
const release = new Date(date);
|
||||
const months = (((current.getFullYear() - release.getFullYear()) * 12) - release.getMonth()) + current.getMonth();
|
||||
|
||||
return Math.max(months, 0);
|
||||
}
|
||||
|
||||
preview(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
@ -40,43 +32,32 @@ export default class StoreCard extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onClick = event => {
|
||||
const {onDetailsView} = this.props;
|
||||
if (typeof(onDetailsView) === "function") onDetailsView();
|
||||
}
|
||||
|
||||
onButtonClick = async (event) => {
|
||||
async onButtonClick(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (this.state.isInstalled) {
|
||||
this.props.deleteAddon(this.props.file_name);
|
||||
this.setState({isInstalled: false});
|
||||
} else {
|
||||
await this.props.installAddon(this.props.id, this.props.file_name, this.props.type);
|
||||
this.setState({isInstalled: true});
|
||||
}
|
||||
} else {
|
||||
if (this.state.isInstalled) {
|
||||
this.props.confirmAddonDelete(this.props.file_name, {
|
||||
onDelete: () => this.setState({isInstalled: false})
|
||||
});
|
||||
} else {
|
||||
Modals.showInstallationModal({...this.props, onInstall: () => {
|
||||
this.setState({isInstalled: true});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (this.props.isInstalled) {
|
||||
if (event.shiftKey) this.props.onForceDelete?.();
|
||||
else this.props.onDelete?.();
|
||||
}
|
||||
else {
|
||||
if (event.shiftKey) this.props.onForceInstall?.();
|
||||
else this.props.onInstall?.();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {name, description, author, selectedTag, tags, likes, downloads, release_date, className} = this.props;
|
||||
const {name, description, author, tags, selectedTag, likes, downloads, releaseDate, thumbnail, className} = this.props;
|
||||
|
||||
return <div className={"bd-store-card" + (className ? ` ${className}` : "")} data-addon-name={name} onClick={this.onClick}>
|
||||
return <div className={"bd-store-card" + (className ? ` ${className}` : "")} data-addon-name={name}>
|
||||
<div className="bd-store-card-header">
|
||||
<div className="bd-store-card-splash">
|
||||
<img key={this.props.thumbnail} onClick={this.preview} alt={name} src={this.props.thumbnail} />
|
||||
<img
|
||||
key={thumbnail}
|
||||
onClick={this.preview.bind(this)}
|
||||
alt={name}
|
||||
src={thumbnail}
|
||||
/>
|
||||
</div>
|
||||
<div className="bd-store-card-icon">
|
||||
<Tooltip color="primary" position="top" text={author.display_name}>
|
||||
|
@ -87,11 +68,11 @@ export default class StoreCard extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className="bd-store-card-body">
|
||||
<div class="bd-store-card-title">
|
||||
<div className="bd-store-card-title">
|
||||
<h5>{name}</h5>
|
||||
{this.monthsAgo <= 3
|
||||
? <Tooltip color="primary" position="top" text={Strings.Addons.uploadDate.format({date: new Date(release_date).toLocaleString()})}>
|
||||
{props => <span {...props} className="bd-store-card-new-badge">{Strings.Addons.new}</span>}
|
||||
{this.monthsAgo(releaseDate) <= 3
|
||||
? <Tooltip color="primary" position="top" text={Strings.Store.uploadDate.format({date: new Date(releaseDate).toLocaleString()})}>
|
||||
{props => <span {...props} className="bd-store-card-new-badge">{Strings.Store.new}</span>}
|
||||
</Tooltip>
|
||||
: null
|
||||
}
|
||||
|
@ -104,7 +85,7 @@ export default class StoreCard extends React.Component {
|
|||
</div>
|
||||
<div className="bd-store-card-footer">
|
||||
<div className="bd-store-card-stats">
|
||||
<Tooltip color="primary" position="top" text={Strings.Addons.likesAmount.format({amount: likes})}>
|
||||
<Tooltip color="primary" position="top" text={Strings.Store.likesAmount.format({amount: likes})}>
|
||||
{props =>
|
||||
<div {...props} className="bd-store-card-stat">
|
||||
<Heart />
|
||||
|
@ -112,7 +93,7 @@ export default class StoreCard extends React.Component {
|
|||
</div>
|
||||
}
|
||||
</Tooltip>
|
||||
<Tooltip color="primary" position="top" text={Strings.Addons.downloadsAmount.format({amount: downloads})}>
|
||||
<Tooltip color="primary" position="top" text={Strings.Store.downloadsAmount.format({amount: downloads})}>
|
||||
{props =>
|
||||
<div {...props} className="bd-store-card-stat">
|
||||
<Download />
|
||||
|
@ -122,11 +103,11 @@ export default class StoreCard extends React.Component {
|
|||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
color={this.state.isInstalled ? Button.Colors.RED : Button.Colors.GREEN}
|
||||
color={this.props.isInstalled ? Button.Colors.RED : Button.Colors.GREEN}
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={this.onButtonClick}
|
||||
onClick={this.onButtonClick.bind(this)}
|
||||
>
|
||||
{this.state.isInstalled ? Strings.Addons.deleteAddon : Strings.Addons.install}
|
||||
{this.props.isInstalled ? Strings.Addons.deleteAddon : Strings.Addons.install}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import {React, WebpackModules} from "modules";
|
||||
import BdWebApi from "../../../modules/bdwebapi";
|
||||
|
||||
const Spinner = WebpackModules.getByDisplayName("Spinner");
|
||||
const {ScrollerAuto: Scroller} = WebpackModules.getByProps("ScrollerAuto");
|
||||
const {pushLayer} = WebpackModules.getByProps("pushLayer");
|
||||
|
||||
export default function openStoreDetail(addon) {
|
||||
pushLayer(() => <StoreDetail {...addon} />);
|
||||
}
|
||||
|
||||
export class StoreDetail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.scrollerRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// dirty hack for customizing layers created by pushLayer
|
||||
this.scrollerRef.current.parentElement.classList.add("bd-store-details");
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<header class="bd-store-details-title">
|
||||
|
||||
</header>
|
||||
<Scroller ref={this.scrollerRef}>
|
||||
<header class="bd-store-details-title"></header>
|
||||
<div class="bd-store-details-content">
|
||||
<Readme type={this.props.type} addonId={this.props.id} />
|
||||
<aside class="bd-store-details-sidebar"></aside>
|
||||
</div>
|
||||
</Scroller>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
class Readme extends React.Component {
|
||||
state = {readme: null};
|
||||
|
||||
async componentDidMount() {
|
||||
const readme = await BdWebApi.getReadme(this.props.type, this.props.addonId);
|
||||
|
||||
this.setState({readme});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {readme} = this.state;
|
||||
return readme ? <article class="bd-store-details-readme bd-markdown" dangerouslySetInnerHTML={{__html: readme}} /> : <Spinner type={Spinner.Type.SPINNING_CIRCLE} />;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ export default class SearchBar extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.state = {
|
||||
hasContent: !!props.value,
|
||||
value: props.value || ""
|
||||
|
@ -28,7 +29,7 @@ export default class SearchBar extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onChange = ({target: {value}}) => {
|
||||
onChange({target: {value}}) {
|
||||
this.setState({value, hasContent: !!value});
|
||||
if (typeof(this.props.onChange) === "function") this.props.onChange(value);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import {React, Utilities} from "modules";
|
||||
|
||||
export default class TabBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
|
||||
this.listRef = React.createRef();
|
||||
this.state = {
|
||||
selected: this.props.items.find(item => item.value === this.props.value)
|
||||
};
|
||||
}
|
||||
|
||||
// getDeriveredStateFromProps(props, state) {
|
||||
// if (props.value !== state.selected) {
|
||||
// return {selected: props.value};
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
onChange(item) {
|
||||
this.setState({selected: item});
|
||||
if (typeof(this.props.onChange) === "function") this.props.onChange(item.value);
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
const children = this.listRef.current.children;
|
||||
const keyMap = {
|
||||
ArrowRight: Array.from(children).indexOf(event.currentTarget) + 1,
|
||||
ArrowLeft: Array.from(children).indexOf(event.currentTarget) - 1,
|
||||
Home: 0,
|
||||
End: children.length - 1
|
||||
};
|
||||
|
||||
if (keyMap.hasOwnProperty(event.key)) {
|
||||
event.preventDefault();
|
||||
children[keyMap[event.key]]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selected} = this.state;
|
||||
|
||||
return <ul role="tablist" className="bd-tab-bar" aria-orientation="horizontal" ref={this.listRef}>
|
||||
{this.props.items.map(item => (
|
||||
<button
|
||||
role="tab"
|
||||
key={item.value}
|
||||
tabIndex={((item.value === selected.value) && !item.disabled) ? "0" : "-1"}
|
||||
className={Utilities.joinClassNames("bd-tab-item", {selected: item.value === selected.value}, {disabled: item.disabled})}
|
||||
onClick={() => this.onChange(item)}
|
||||
onKeyDown={this.onKeyDown}
|
||||
aria-disabled={item.disabled}
|
||||
aria-selected={item.value === selected.value}
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
{this.props.children}
|
||||
</ul>;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue