mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eba79f4445 | ||
|
|
e3bb9881ea | ||
|
|
827049cb35 | ||
|
|
ad8100d402 | ||
|
|
7b5defff16 | ||
|
|
bc9805d1ba | ||
|
|
c823016b36 | ||
|
|
ca0f4c6fba | ||
|
|
c0d2f2d852 |
@@ -1,5 +1,9 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
import NextAuth from "next-auth"
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
import GitHubProvider from "next-auth/providers/github"
|
||||
import Auth0Provider from "next-auth/providers/auth0"
|
||||
import TwitterProvider from "next-auth/providers/twitter"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
@@ -28,15 +32,15 @@ export default NextAuth({
|
||||
// }
|
||||
// },
|
||||
providers: [
|
||||
Providers.Email({
|
||||
EmailProvider({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
Providers.GitHub({
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Providers.Auth0({
|
||||
Auth0Provider({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
@@ -45,36 +49,36 @@ export default NextAuth({
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: 'pkce'
|
||||
protection: "pkce",
|
||||
}),
|
||||
Providers.Twitter({
|
||||
TwitterProvider({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
clientSecret: process.env.TWITTER_SECRET,
|
||||
}),
|
||||
Providers.Credentials({
|
||||
name: 'Credentials',
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
password: { label: 'Password', type: 'password' }
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize (credentials) {
|
||||
if (credentials.password === 'password') {
|
||||
async authorize(credentials) {
|
||||
if (credentials.password === "password") {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Fill Murray',
|
||||
email: 'bill@fillmurray.com',
|
||||
image: 'https://www.fillmurray.com/64/64'
|
||||
name: "Fill Murray",
|
||||
email: "bill@fillmurray.com",
|
||||
image: "https://www.fillmurray.com/64/64",
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET
|
||||
secret: process.env.SECRET,
|
||||
},
|
||||
debug: false,
|
||||
theme: 'auto'
|
||||
theme: "auto",
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
|
||||
1741
package-lock.json
generated
1741
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -38,7 +38,7 @@
|
||||
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test:types": "dtslint types",
|
||||
"test:types": "dtslint types --onlyTestTsNext",
|
||||
"prepublishOnly": "npm run build",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
@@ -64,7 +64,6 @@
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"@next-auth/prisma-legacy-adapter": "canary",
|
||||
"@next-auth/typeorm-legacy-adapter": "canary",
|
||||
"crypto-js": "^4.0.0",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
@@ -73,9 +72,7 @@
|
||||
"pkce-challenge": "^2.1.0",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"querystring": "^0.2.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.30"
|
||||
"querystring": "^0.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17",
|
||||
@@ -94,7 +91,6 @@
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
@@ -115,19 +111,10 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"mocha": "^8.1.3",
|
||||
"mongodb": "^3.5.9",
|
||||
"mssql": "^6.2.1",
|
||||
"mysql": "^2.18.1",
|
||||
"next": "^10.0.5",
|
||||
"pg": "^8.2.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"prisma": "^2.16.1",
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import TypeORM from './typeorm'
|
||||
import Prisma from './prisma'
|
||||
import * as TypeORM from "./typeorm"
|
||||
import * as Prisma from "./prisma"
|
||||
|
||||
export { TypeORM, Prisma }
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma
|
||||
Prisma,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
|
||||
*/
|
||||
|
||||
import PrismaLegacyAdapter from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
export default PrismaLegacyAdapter
|
||||
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
|
||||
*/
|
||||
|
||||
import TypeORMLegacyAdapter from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
export default TypeORMLegacyAdapter
|
||||
export {
|
||||
TypeORMLegacyAdapter as Adapter,
|
||||
Models,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
31
types/adapters.d.ts
vendored
31
types/adapters.d.ts
vendored
@@ -1,13 +1,36 @@
|
||||
import { AppOptions } from "./internals"
|
||||
import { User, Profile, Session } from "."
|
||||
import { EmailConfig } from "./providers"
|
||||
import { ConnectionOptions } from "typeorm"
|
||||
|
||||
/** Legacy */
|
||||
|
||||
export {
|
||||
TypeORMAccountModel,
|
||||
TypeORMSessionModel,
|
||||
TypeORMUserModel,
|
||||
TypeORMVerificationRequestModel,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
import {
|
||||
TypeORMAdapter,
|
||||
TypeORMAdapterModels,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
export const TypeORM: {
|
||||
Models: TypeORMAdapterModels
|
||||
Adapter: TypeORMAdapter
|
||||
}
|
||||
|
||||
export const Prisma: {
|
||||
Adapter: PrismaLegacyAdapter
|
||||
}
|
||||
|
||||
declare const Adapters: {
|
||||
Default: Adapter<ConnectionOptions>
|
||||
TypeORM: { Adapter: Adapter<ConnectionOptions> }
|
||||
Prisma: { Adapter: Adapter }
|
||||
Default: TypeORMAdapter
|
||||
TypeORM: typeof TypeORM
|
||||
Prisma: typeof Prisma
|
||||
}
|
||||
export default Adapters
|
||||
|
||||
|
||||
@@ -156,9 +156,9 @@ Check out the content of all the params in addition `token`, to see what info yo
|
||||
:::
|
||||
|
||||
:::warning
|
||||
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** for all cookies on a domain is commonly imposed by browsers.
|
||||
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** per cookie is commonly imposed by browsers.
|
||||
|
||||
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
|
||||
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback.
|
||||
:::
|
||||
|
||||
## Session callback
|
||||
|
||||
@@ -196,9 +196,9 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
|
||||
|
||||
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expiring if there is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
|
||||
|
||||
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes in total for all cookies on a domain, though the exact limit varies between browsers, proxies and hosting services.
|
||||
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes per cookie, though the exact limit varies between browsers, proxies and hosting services. If you want to support most browsers, then do not exceed 4096 bytes per cookie. If you want to save more data, you will need to persist your sessions in a database (Source: [browsercookielimits.iain.guru](http://browsercookielimits.iain.guru/))
|
||||
|
||||
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~2 KB of data you probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server side key/value store).
|
||||
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~4 KB of data you're probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server-side key/value store).
|
||||
|
||||
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
|
||||
|
||||
|
||||
24167
www/package-lock.json
generated
24167
www/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,23 +4,26 @@
|
||||
"scripts": {
|
||||
"start": "npm run generate-providers && docusaurus start",
|
||||
"build": "npm run generate-providers && docusaurus build",
|
||||
"docusaurus": "docusaurus",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"serve": "docusaurus serve",
|
||||
"clear": "docusaurus clear",
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix",
|
||||
"generate-providers": "node ./scripts/generate-providers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-alpha.70",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.70",
|
||||
"classnames": "^2.2.6",
|
||||
"docusaurus-lunr-search": "^2.1.10",
|
||||
"jose": "^2.0.2",
|
||||
"@docusaurus/core": "2.0.0-beta.0",
|
||||
"@docusaurus/preset-classic": "2.0.0-beta.0",
|
||||
"classnames": "^2.3.1",
|
||||
"docusaurus-lunr-search": "^2.1.14",
|
||||
"jose": "^2.0.5",
|
||||
"lodash.times": "^4.3.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-marquee-slider": "^1.1.2",
|
||||
"styled-components": "^5.2.1"
|
||||
"styled-components": "^5.2.3"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
|
||||
html[data-theme="dark"]:root {
|
||||
--ifm-color-link: #289ef9;
|
||||
--ifm-footer-background-color: #111;
|
||||
--ifm-footer-background-color: #000;
|
||||
--ifm-html-background-color: #242526;
|
||||
--ifm-background-color: #000000;
|
||||
--ifm-background-color: #090909;
|
||||
--ifm-hero-background-color: #111111;
|
||||
--ifm-navbar-background-color: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
@@ -198,6 +198,21 @@ html[data-theme="dark"] hr {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.navbar__items .react-toggle {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.navbar__search-input:focus {
|
||||
outline: none;
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgba(19, 19, 19, 0.2) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px;
|
||||
transition: box-shadow 350ms ease-in-out;
|
||||
}
|
||||
html[data-theme="dark"] .navbar__search-input:focus {
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgba(29, 29, 29, 0.5) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .navbar__item.navbar__link[href*="github"]:before {
|
||||
background-image: url("/img/brand-github-inverted.svg");
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
margin-right: -4px;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
content: '';
|
||||
content: "";
|
||||
}
|
||||
|
||||
.searchbox__submit:hover,
|
||||
@@ -237,7 +237,7 @@
|
||||
.algolia-autocomplete .ds-dropdown-menu:before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
content: '';
|
||||
content: "";
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #373940;
|
||||
@@ -308,7 +308,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.algolia-autocomplete .ds-dropdown-menu [class^='ds-dataset-'] {
|
||||
.algolia-autocomplete .ds-dropdown-menu [class^="ds-dataset-"] {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
@@ -369,7 +369,7 @@
|
||||
}
|
||||
|
||||
.algolia-autocomplete .algolia-docsearch-suggestion--content:before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
@@ -413,7 +413,7 @@
|
||||
}
|
||||
|
||||
.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
@@ -484,7 +484,7 @@
|
||||
color: #222222;
|
||||
background-color: #ebebeb;
|
||||
border-radius: 3px;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { useRef, useCallback } from "react";
|
||||
import classnames from "classnames";
|
||||
import { useHistory } from "@docusaurus/router";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
let loaded = false;
|
||||
import React, { useRef, useCallback, useEffect } from "react"
|
||||
import classnames from "classnames"
|
||||
import { useHistory } from "@docusaurus/router"
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
|
||||
import "./styles.css"
|
||||
|
||||
let loaded = false
|
||||
const Search = (props) => {
|
||||
const initialized = useRef(false);
|
||||
const searchBarRef = useRef(null);
|
||||
const history = useHistory();
|
||||
const { siteConfig = {} } = useDocusaurusContext();
|
||||
const { baseUrl } = siteConfig;
|
||||
const initialized = useRef(false)
|
||||
const searchBarRef = useRef(null)
|
||||
const history = useHistory()
|
||||
const { siteConfig = {} } = useDocusaurusContext()
|
||||
const { baseUrl } = siteConfig
|
||||
const initAlgolia = () => {
|
||||
if (!initialized.current) {
|
||||
new window.DocSearch({
|
||||
@@ -25,25 +27,25 @@ const Search = (props) => {
|
||||
// Override algolia's default selection event, allowing us to do client-side
|
||||
// navigation and avoiding a full page refresh.
|
||||
handleSelected: (_input, _event, suggestion) => {
|
||||
const url = baseUrl + suggestion.url;
|
||||
const url = baseUrl + suggestion.url
|
||||
// Use an anchor tag to parse the absolute url into a relative url
|
||||
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
|
||||
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
|
||||
|
||||
history.push(url);
|
||||
history.push(url)
|
||||
},
|
||||
});
|
||||
initialized.current = true;
|
||||
})
|
||||
initialized.current = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getSearchData = () =>
|
||||
process.env.NODE_ENV === "production"
|
||||
? fetch(`${baseUrl}search-doc.json`).then((content) => content.json())
|
||||
: Promise.resolve([]);
|
||||
: Promise.resolve([])
|
||||
|
||||
const loadAlgolia = () => {
|
||||
if (!loaded) {
|
||||
@@ -52,26 +54,38 @@ const Search = (props) => {
|
||||
import("./lib/DocSearch"),
|
||||
import("./algolia.css"),
|
||||
]).then(([searchData, { default: DocSearch }]) => {
|
||||
loaded = true;
|
||||
window.searchData = searchData;
|
||||
window.DocSearch = DocSearch;
|
||||
initAlgolia();
|
||||
});
|
||||
loaded = true
|
||||
window.searchData = searchData
|
||||
window.DocSearch = DocSearch
|
||||
initAlgolia()
|
||||
})
|
||||
} else {
|
||||
initAlgolia();
|
||||
initAlgolia()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const toggleSearchIconClick = useCallback(
|
||||
(e) => {
|
||||
if (!searchBarRef.current.contains(e.target)) {
|
||||
searchBarRef.current.focus();
|
||||
searchBarRef.current.focus()
|
||||
}
|
||||
|
||||
props.handleSearchBarToggle(!props.isSearchBarExpanded);
|
||||
props.handleSearchBarToggle &&
|
||||
props.handleSearchBarToggle(!props.isSearchBarExpanded)
|
||||
},
|
||||
[props.isSearchBarExpanded]
|
||||
);
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keypress", (e) => {
|
||||
if (document.activeElement === searchBarRef.current) return
|
||||
if (e.key === "/") {
|
||||
e.preventDefault()
|
||||
searchBarRef.current.focus()
|
||||
}
|
||||
})
|
||||
return () => document.removeEventListener("keypress")
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="navbar__search" key="search-box">
|
||||
@@ -101,8 +115,27 @@ const Search = (props) => {
|
||||
onBlur={toggleSearchIconClick}
|
||||
ref={searchBarRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
<svg
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="19px"
|
||||
height="20px"
|
||||
viewBox="0 0 19 20"
|
||||
className={classnames("search-icon-keyboard", {
|
||||
"search-icon-hidden": props.isSearchBarExpanded,
|
||||
})}
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#979A9C"
|
||||
opacity="0.4"
|
||||
d="M3.5,0.5h12c1.7,0,3,1.3,3,3v13c0,1.7-1.3,3-3,3h-12c-1.7,0-3-1.3-3-3v-13C0.5,1.8,1.8,0.5,3.5,0.5z"
|
||||
/>
|
||||
<path fill="#979a9c" d="M11.8,6L8,15.1H7.1L10.8,6L11.8,6z" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Search
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Hogan from 'hogan.js'
|
||||
import LunrSearchAdapter from './lunar-search'
|
||||
import autocomplete from 'autocomplete.js'
|
||||
import templates from './templates'
|
||||
import utils from './utils'
|
||||
import $ from './zepto'
|
||||
import Hogan from "hogan.js"
|
||||
import LunrSearchAdapter from "./lunar-search"
|
||||
import autocomplete from "autocomplete.js"
|
||||
import templates from "./templates"
|
||||
import utils from "./utils"
|
||||
import $ from "./zepto"
|
||||
|
||||
/**
|
||||
* Adds an autocomplete dropdown to an input field
|
||||
@@ -22,7 +22,7 @@ const usage = `Usage:
|
||||
[ autocompleteOptions.{hint,debug} ]
|
||||
})`
|
||||
class DocSearch {
|
||||
constructor ({
|
||||
constructor({
|
||||
searchData,
|
||||
inputSelector,
|
||||
debug = false,
|
||||
@@ -30,40 +30,40 @@ class DocSearch {
|
||||
autocompleteOptions = {
|
||||
debug: false,
|
||||
hint: false,
|
||||
autoselect: true
|
||||
autoselect: true,
|
||||
},
|
||||
transformData = false,
|
||||
queryHook = false,
|
||||
handleSelected = false,
|
||||
enhancedSearchInput = false,
|
||||
layout = 'collumns'
|
||||
layout = "collumns",
|
||||
}) {
|
||||
DocSearch.checkArguments({
|
||||
searchData,
|
||||
inputSelector
|
||||
inputSelector,
|
||||
})
|
||||
this.searchData = searchData
|
||||
this.input = DocSearch.getInputFromSelector(inputSelector)
|
||||
this.queryDataCallback = queryDataCallback || null
|
||||
const autocompleteOptionsDebug =
|
||||
autocompleteOptions && autocompleteOptions.debug
|
||||
? autocompleteOptions.debug
|
||||
: false
|
||||
autocompleteOptions && autocompleteOptions.debug
|
||||
? autocompleteOptions.debug
|
||||
: false
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
autocompleteOptions.debug = debug || autocompleteOptionsDebug
|
||||
this.autocompleteOptions = autocompleteOptions
|
||||
this.autocompleteOptions.cssClasses =
|
||||
this.autocompleteOptions.cssClasses || {}
|
||||
this.autocompleteOptions.cssClasses || {}
|
||||
this.autocompleteOptions.cssClasses.prefix =
|
||||
this.autocompleteOptions.cssClasses.prefix || 'ds'
|
||||
this.autocompleteOptions.cssClasses.prefix || "ds"
|
||||
const inputAriaLabel =
|
||||
this.input &&
|
||||
typeof this.input.attr === 'function' &&
|
||||
this.input.attr('aria-label')
|
||||
this.input &&
|
||||
typeof this.input.attr === "function" &&
|
||||
this.input.attr("aria-label")
|
||||
this.autocompleteOptions.ariaLabel =
|
||||
this.autocompleteOptions.ariaLabel || inputAriaLabel || 'search input'
|
||||
this.autocompleteOptions.ariaLabel || inputAriaLabel || "search input"
|
||||
|
||||
this.isSimpleLayout = layout === 'simple'
|
||||
this.isSimpleLayout = layout === "simple"
|
||||
|
||||
this.client = new LunrSearchAdapter(this.searchData)
|
||||
|
||||
@@ -76,9 +76,9 @@ class DocSearch {
|
||||
templates: {
|
||||
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
|
||||
footer: templates.footer,
|
||||
empty: DocSearch.getEmptyTemplate()
|
||||
}
|
||||
}
|
||||
empty: DocSearch.getEmptyTemplate(),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const customHandleSelected = handleSelected
|
||||
@@ -86,18 +86,18 @@ class DocSearch {
|
||||
|
||||
// We prevent default link clicking if a custom handleSelected is defined
|
||||
if (customHandleSelected) {
|
||||
$('.algolia-autocomplete').on('click', '.ds-suggestions a', event => {
|
||||
$(".algolia-autocomplete").on("click", ".ds-suggestions a", (event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
}
|
||||
|
||||
this.autocomplete.on(
|
||||
'autocomplete:selected',
|
||||
"autocomplete:selected",
|
||||
this.handleSelected.bind(null, this.autocomplete.autocomplete)
|
||||
)
|
||||
|
||||
this.autocomplete.on(
|
||||
'autocomplete:shown',
|
||||
"autocomplete:shown",
|
||||
this.handleShown.bind(null, this.input)
|
||||
)
|
||||
|
||||
@@ -107,87 +107,84 @@ class DocSearch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the passed arguments are valid. Will throw errors otherwise
|
||||
* @function checkArguments
|
||||
* @param {object} args Arguments as an option object
|
||||
* @returns {void}
|
||||
*/
|
||||
static checkArguments (args) {
|
||||
* Checks that the passed arguments are valid. Will throw errors otherwise
|
||||
* @function checkArguments
|
||||
* @param {object} args Arguments as an option object
|
||||
* @returns {void}
|
||||
*/
|
||||
static checkArguments(args) {
|
||||
if (!args.searchData) {
|
||||
throw new Error(usage)
|
||||
}
|
||||
|
||||
if (typeof args.inputSelector !== 'string') {
|
||||
if (typeof args.inputSelector !== "string") {
|
||||
throw new Error(
|
||||
`Error: inputSelector:${args.inputSelector} must be a string. Each selector must match only one element and separated by ','`
|
||||
`Error: inputSelector:${args.inputSelector} must be a string. Each selector must match only one element and separated by ','`
|
||||
)
|
||||
}
|
||||
|
||||
if (!DocSearch.getInputFromSelector(args.inputSelector)) {
|
||||
throw new Error(
|
||||
`Error: No input element in the page matches ${args.inputSelector}`
|
||||
`Error: No input element in the page matches ${args.inputSelector}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static injectSearchBox (input) {
|
||||
static injectSearchBox(input) {
|
||||
input.before(templates.searchBox)
|
||||
const newInput = input
|
||||
.prev()
|
||||
.prev()
|
||||
.find('input')
|
||||
const newInput = input.prev().prev().find("input")
|
||||
input.remove()
|
||||
return newInput
|
||||
}
|
||||
|
||||
static bindSearchBoxEvent () {
|
||||
$('.searchbox [type="reset"]').on('click', function () {
|
||||
$('input#docsearch').focus()
|
||||
$(this).addClass('hide')
|
||||
autocomplete.autocomplete.setVal('')
|
||||
static bindSearchBoxEvent() {
|
||||
$('.searchbox [type="reset"]').on("click", function () {
|
||||
$("input#docsearch").focus()
|
||||
$(this).addClass("hide")
|
||||
autocomplete.autocomplete.setVal("")
|
||||
})
|
||||
|
||||
$('input#docsearch').on('keyup', () => {
|
||||
const searchbox = document.querySelector('input#docsearch')
|
||||
$("input#docsearch").on("keyup", () => {
|
||||
const searchbox = document.querySelector("input#docsearch")
|
||||
const reset = document.querySelector('.searchbox [type="reset"]')
|
||||
reset.className = 'searchbox__reset'
|
||||
reset.className = "searchbox__reset hide"
|
||||
if (searchbox.value.length === 0) {
|
||||
reset.className += ' hide'
|
||||
reset.className += " hide"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the matching input from a CSS selector, null if none matches
|
||||
* @function getInputFromSelector
|
||||
* @param {string} selector CSS selector that matches the search
|
||||
* input of the page
|
||||
* @returns {void}
|
||||
*/
|
||||
static getInputFromSelector (selector) {
|
||||
const input = $(selector).filter('input')
|
||||
* Returns the matching input from a CSS selector, null if none matches
|
||||
* @function getInputFromSelector
|
||||
* @param {string} selector CSS selector that matches the search
|
||||
* input of the page
|
||||
* @returns {void}
|
||||
*/
|
||||
static getInputFromSelector(selector) {
|
||||
const input = $(selector).filter("input")
|
||||
return input.length ? $(input[0]) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `source` method to be passed to autocomplete.js. It will query
|
||||
* the Algolia index and call the callbacks with the formatted hits.
|
||||
* @function getAutocompleteSource
|
||||
* @param {function} transformData An optional function to transform the hits
|
||||
* @param {function} queryHook An optional function to transform the query
|
||||
* @returns {function} Method to be passed as the `source` option of
|
||||
* autocomplete
|
||||
*/
|
||||
getAutocompleteSource (transformData, queryHook) {
|
||||
* Returns the `source` method to be passed to autocomplete.js. It will query
|
||||
* the Algolia index and call the callbacks with the formatted hits.
|
||||
* @function getAutocompleteSource
|
||||
* @param {function} transformData An optional function to transform the hits
|
||||
* @param {function} queryHook An optional function to transform the query
|
||||
* @returns {function} Method to be passed as the `source` option of
|
||||
* autocomplete
|
||||
*/
|
||||
getAutocompleteSource(transformData, queryHook) {
|
||||
return (query, callback) => {
|
||||
if (queryHook) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
query = queryHook(query) || query
|
||||
}
|
||||
this.client.search(query).then(hits => {
|
||||
this.client.search(query).then((hits) => {
|
||||
if (
|
||||
this.queryDataCallback &&
|
||||
typeof this.queryDataCallback === 'function'
|
||||
typeof this.queryDataCallback === "function"
|
||||
) {
|
||||
this.queryDataCallback(hits)
|
||||
}
|
||||
@@ -201,58 +198,57 @@ class DocSearch {
|
||||
|
||||
// Given a list of hits returned by the API, will reformat them to be used in
|
||||
// a Hogan template
|
||||
static formatHits (receivedHits) {
|
||||
static formatHits(receivedHits) {
|
||||
const clonedHits = utils.deepClone(receivedHits)
|
||||
const hits = clonedHits.map(hit => {
|
||||
const hits = clonedHits.map((hit) => {
|
||||
if (hit._highlightResult) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
hit._highlightResult = utils.mergeKeyWithParent(
|
||||
hit._highlightResult,
|
||||
'hierarchy'
|
||||
"hierarchy"
|
||||
)
|
||||
}
|
||||
return utils.mergeKeyWithParent(hit, 'hierarchy')
|
||||
return utils.mergeKeyWithParent(hit, "hierarchy")
|
||||
})
|
||||
|
||||
// Group hits by category / subcategory
|
||||
let groupedHits = utils.groupBy(hits, 'lvl0')
|
||||
let groupedHits = utils.groupBy(hits, "lvl0")
|
||||
$.each(groupedHits, (level, collection) => {
|
||||
const groupedHitsByLvl1 = utils.groupBy(collection, 'lvl1')
|
||||
const groupedHitsByLvl1 = utils.groupBy(collection, "lvl1")
|
||||
const flattenedHits = utils.flattenAndFlagFirst(
|
||||
groupedHitsByLvl1,
|
||||
'isSubCategoryHeader'
|
||||
"isSubCategoryHeader"
|
||||
)
|
||||
groupedHits[level] = flattenedHits
|
||||
})
|
||||
groupedHits = utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader')
|
||||
groupedHits = utils.flattenAndFlagFirst(groupedHits, "isCategoryHeader")
|
||||
|
||||
// Translate hits into smaller objects to be send to the template
|
||||
return groupedHits.map(hit => {
|
||||
return groupedHits.map((hit) => {
|
||||
const url = DocSearch.formatURL(hit)
|
||||
const category = utils.getHighlightedValue(hit, 'lvl0')
|
||||
const subcategory = utils.getHighlightedValue(hit, 'lvl1') || category
|
||||
const category = utils.getHighlightedValue(hit, "lvl0")
|
||||
const subcategory = utils.getHighlightedValue(hit, "lvl1") || category
|
||||
const displayTitle = utils
|
||||
.compact([
|
||||
utils.getHighlightedValue(hit, 'lvl2') || subcategory,
|
||||
utils.getHighlightedValue(hit, 'lvl3'),
|
||||
utils.getHighlightedValue(hit, 'lvl4'),
|
||||
utils.getHighlightedValue(hit, 'lvl5'),
|
||||
utils.getHighlightedValue(hit, 'lvl6')
|
||||
utils.getHighlightedValue(hit, "lvl2") || subcategory,
|
||||
utils.getHighlightedValue(hit, "lvl3"),
|
||||
utils.getHighlightedValue(hit, "lvl4"),
|
||||
utils.getHighlightedValue(hit, "lvl5"),
|
||||
utils.getHighlightedValue(hit, "lvl6"),
|
||||
])
|
||||
.join(
|
||||
'<span class="aa-suggestion-title-separator" aria-hidden="true"> › </span>'
|
||||
)
|
||||
const text = utils.getSnippetedValue(hit, 'content')
|
||||
const text = utils.getSnippetedValue(hit, "content")
|
||||
const isTextOrSubcategoryNonEmpty =
|
||||
(subcategory && subcategory !== '') ||
|
||||
(displayTitle && displayTitle !== '')
|
||||
(subcategory && subcategory !== "") ||
|
||||
(displayTitle && displayTitle !== "")
|
||||
const isLvl1EmptyOrDuplicate =
|
||||
!subcategory || subcategory === '' || subcategory === category
|
||||
!subcategory || subcategory === "" || subcategory === category
|
||||
const isLvl2 =
|
||||
displayTitle && displayTitle !== '' && displayTitle !== subcategory
|
||||
displayTitle && displayTitle !== "" && displayTitle !== subcategory
|
||||
const isLvl1 =
|
||||
!isLvl2 &&
|
||||
(subcategory && subcategory !== '' && subcategory !== category)
|
||||
!isLvl2 && subcategory && subcategory !== "" && subcategory !== category
|
||||
const isLvl0 = !isLvl1 && !isLvl2
|
||||
|
||||
return {
|
||||
@@ -267,50 +263,50 @@ class DocSearch {
|
||||
subcategory,
|
||||
title: displayTitle,
|
||||
text,
|
||||
url
|
||||
url,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static formatURL (hit) {
|
||||
static formatURL(hit) {
|
||||
const { url, anchor } = hit
|
||||
if (url) {
|
||||
const containsAnchor = url.indexOf('#') !== -1
|
||||
const containsAnchor = url.indexOf("#") !== -1
|
||||
if (containsAnchor) return url
|
||||
else if (anchor) return `${hit.url}#${hit.anchor}`
|
||||
return url
|
||||
} else if (anchor) return `#${hit.anchor}`
|
||||
/* eslint-disable */
|
||||
console.warn("no anchor nor url for : ", JSON.stringify(hit));
|
||||
/* eslint-enable */
|
||||
console.warn("no anchor nor url for : ", JSON.stringify(hit))
|
||||
/* eslint-enable */
|
||||
return null
|
||||
}
|
||||
|
||||
static getEmptyTemplate () {
|
||||
return args => Hogan.compile(templates.empty).render(args)
|
||||
static getEmptyTemplate() {
|
||||
return (args) => Hogan.compile(templates.empty).render(args)
|
||||
}
|
||||
|
||||
static getSuggestionTemplate (isSimpleLayout) {
|
||||
static getSuggestionTemplate(isSimpleLayout) {
|
||||
const stringTemplate = isSimpleLayout
|
||||
? templates.suggestionSimple
|
||||
: templates.suggestion
|
||||
const template = Hogan.compile(stringTemplate)
|
||||
return suggestion => template.render(suggestion)
|
||||
return (suggestion) => template.render(suggestion)
|
||||
}
|
||||
|
||||
handleSelected (input, event, suggestion, datasetNumber, context = {}) {
|
||||
handleSelected(input, event, suggestion, datasetNumber, context = {}) {
|
||||
// Do nothing if click on the suggestion, as it's already a <a href>, the
|
||||
// browser will take care of it. This allow Ctrl-Clicking on results and not
|
||||
// having the main window being redirected as well
|
||||
if (context.selectionMethod === 'click') {
|
||||
if (context.selectionMethod === "click") {
|
||||
return
|
||||
}
|
||||
|
||||
input.setVal('')
|
||||
input.setVal("")
|
||||
window.location.assign(suggestion.url)
|
||||
}
|
||||
|
||||
handleShown (input) {
|
||||
handleShown(input) {
|
||||
const middleOfInput = input.offset().left + input.width() / 2
|
||||
let middleOfWindow = $(document).width() / 2
|
||||
|
||||
@@ -319,14 +315,14 @@ class DocSearch {
|
||||
}
|
||||
|
||||
const alignClass =
|
||||
middleOfInput - middleOfWindow >= 0
|
||||
? 'algolia-autocomplete-right'
|
||||
: 'algolia-autocomplete-left'
|
||||
middleOfInput - middleOfWindow >= 0
|
||||
? "algolia-autocomplete-right"
|
||||
: "algolia-autocomplete-left"
|
||||
const otherAlignClass =
|
||||
middleOfInput - middleOfWindow < 0
|
||||
? 'algolia-autocomplete-right'
|
||||
: 'algolia-autocomplete-left'
|
||||
const autocompleteWrapper = $('.algolia-autocomplete')
|
||||
middleOfInput - middleOfWindow < 0
|
||||
? "algolia-autocomplete-right"
|
||||
: "algolia-autocomplete-left"
|
||||
const autocompleteWrapper = $(".algolia-autocomplete")
|
||||
if (!autocompleteWrapper.hasClass(alignClass)) {
|
||||
autocompleteWrapper.addClass(alignClass)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const prefix = 'algolia-docsearch'
|
||||
const prefix = "algolia-docsearch"
|
||||
const suggestionPrefix = `${prefix}-suggestion`
|
||||
const footerPrefix = `${prefix}-footer`
|
||||
|
||||
@@ -89,26 +89,22 @@ const templates = {
|
||||
<form novalidate="novalidate" onsubmit="return false;" class="searchbox">
|
||||
<div role="search" class="searchbox__wrapper">
|
||||
<input id="docsearch" type="search" name="search" placeholder="Search the docs" autocomplete="off" required="required" class="searchbox__input"/>
|
||||
|
||||
<button type="submit" title="Submit your search query." class="searchbox__submit" >
|
||||
<svg width=12 height=12 role="img" aria-label="Search">
|
||||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-search-13"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="reset" title="Clear the search query." class="searchbox__reset hide">
|
||||
<svg width=12 height=12 role="img" aria-label="Reset">
|
||||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-clear-3"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="svg-icons" style="height: 0; width: 0; position: absolute; visibility: hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"><path d="M16.228 20L1.886 5.657 0 3.772 3.772 0l1.885 1.886L20 16.228 34.343 1.886 36.228 0 40 3.772l-1.886 1.885L23.772 20l14.342 14.343L40 36.228 36.228 40l-1.885-1.886L20 23.772 5.657 38.114 3.772 40 0 36.228l1.886-1.885L16.228 20z" fill-rule="evenodd"></symbol>
|
||||
<symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"></symbol>
|
||||
<symbol id="sbx-icon-search-13" viewBox="0 0 40 40"><path d="M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.332 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.807 29.012zm-10.427.627c7.322 0 13.26-5.938 13.26-13.26 0-7.324-5.938-13.26-13.26-13.26-7.324 0-13.26 5.936-13.26 13.26 0 7.322 5.936 13.26 13.26 13.26z" fill-rule="evenodd"></symbol>
|
||||
</svg>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
}
|
||||
|
||||
export default templates
|
||||
|
||||
@@ -17,10 +17,22 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-icon-keyboard {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.search-icon-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.searchbox__input::-webkit-search-decoration,
|
||||
.searchbox__input::-webkit-search-cancel-button {
|
||||
visibility: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.search-bar {
|
||||
width: 0 !important;
|
||||
@@ -37,4 +49,13 @@
|
||||
display: inline;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.search-icon-keyboard {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user