diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..f22b51307 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +# eslint-plugin-import의 no-unused-modules 규칙이 FileEnumerator 내부 API를 사용할 때 +# .eslintignore 파일의 존재를 필요로 합니다. 내용이 없어도 파일이 있어야 정상 동작합니다. diff --git a/.github/workflows/assign-reviewer.yml b/.github/workflows/assign-reviewer.yml deleted file mode 100644 index 743b83c93..000000000 --- a/.github/workflows/assign-reviewer.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Assign Reviewer - -on: - pull_request: - types: [opened, ready_for_review] - -jobs: - assign: - runs-on: ubuntu-latest - steps: - - uses: hkusu/review-assign-action@v1 - with: - assignees: ${{ github.actor }} - # reviewers: TODO: 팀 전체 리뷰어 목록으로 업데이트 diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index af450a784..0d8036031 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -37,6 +37,9 @@ jobs: cp apps/playground/.env.development apps/playground/.env.local echo "${{ vars.PLAYGROUND_DEVELOPMENT_ENV }}" >> apps/playground/.env.local + - name: Build packages + run: yarn turbo run build --filter=@sopt/ui + - name: Publish Chromatic id: publish_chromatic uses: chromaui/action@v1 diff --git a/.github/workflows/crew-deploy-development.yml b/.github/workflows/crew-deploy-development.yml index f9893483d..fe839992b 100644 --- a/.github/workflows/crew-deploy-development.yml +++ b/.github/workflows/crew-deploy-development.yml @@ -4,14 +4,11 @@ on: push: branches: [develop] paths: - - "apps/crew/**" - - "packages/**" - - "package.json" - - "yarn.lock" + - 'apps/crew/**' + - 'packages/**' + - 'package.json' + - 'yarn.lock' pull_request: - paths: - - "apps/crew/**" - - "packages/**" workflow_dispatch: jobs: diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index fceff3694..9c3540b84 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -16,7 +16,7 @@ jobs: - name: Label PR run: | PR=${{ github.event.pull_request.number }} - changed_files=$(gh pr diff $PR --name-only) + changed_files=$(gh api "repos/${{ github.repository }}/pulls/$PR/files" --paginate -q '.[].filename') current_labels=$(gh pr view $PR --json labels -q '.labels[].name') diff --git a/.github/workflows/playground-deploy-development.yml b/.github/workflows/playground-deploy-development.yml index e68945a23..71a9bc6c8 100644 --- a/.github/workflows/playground-deploy-development.yml +++ b/.github/workflows/playground-deploy-development.yml @@ -4,14 +4,11 @@ on: push: branches: [develop] paths: - - "apps/playground/**" - - "packages/**" - - "package.json" - - "yarn.lock" + - 'apps/playground/**' + - 'packages/**' + - 'package.json' + - 'yarn.lock' pull_request: - paths: - - "apps/playground/**" - - "packages/**" workflow_dispatch: jobs: @@ -84,8 +81,8 @@ jobs: id: find_comment with: issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: "Playground 프리뷰 배포 확인하기" + comment-author: 'github-actions[bot]' + body-includes: 'Playground 프리뷰 배포 확인하기' - name: Create or update comment if: github.event_name == 'pull_request' diff --git a/.gitignore b/.gitignore index 638f571d5..d9301268a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,57 @@ +# yarn .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions +.pnp.* +install-state.gz -# Whether you use PnP or not, the node_modules folder is often used to store -# build artifacts that should be gitignored +# dependencies node_modules -# Swap the comments on the following lines if you wish to use zero-installs -# In that case, don't forget to run `yarn config set enableGlobalCache false`! -# Documentation here: https://yarnpkg.com/features/caching#zero-installs +# next.js +.next/ +out/ -#!.yarn/cache -.pnp.* +# production +build/ +coverage/ + +# storybook +storybook-static + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* -.env.local +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# editor +.vscode/ +.idea # Turbo .turbo # Build outputs -packages/ui/dist \ No newline at end of file +packages/ui/dist/**/* + +# crew generated +apps/crew/src/__generated__/open-api-specification.json \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..5a182ef10 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 000000000..e6f1510ef --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,5 @@ +{ + "apps/crew/**/*.{ts,tsx}": ["eslint --fix", "yarn workspace crew typecheck"], + "apps/playground/**/*.{ts,tsx}": ["eslint --fix", "yarn workspace playground typecheck"], + "**/*.{ts,tsx,js,jsx,json,css,md}": "prettier --write --ignore-path .prettierignore" +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..0320b1d27 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# dependencies +node_modules + +# build outputs +.next +out +dist + +# docs +**/*.md diff --git a/apps/playground/.prettierrc.json b/.prettierrc similarity index 92% rename from apps/playground/.prettierrc.json rename to .prettierrc index c476eb116..19ad21a3c 100644 --- a/apps/playground/.prettierrc.json +++ b/.prettierrc @@ -4,6 +4,7 @@ "jsxSingleQuote": true, "printWidth": 120, "quoteProps": "consistent", + "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "all" diff --git a/apps/crew/.eslintignore b/apps/crew/.eslintignore index 3d85ec336..f22b51307 100644 --- a/apps/crew/.eslintignore +++ b/apps/crew/.eslintignore @@ -1 +1,2 @@ -src/__generated__ \ No newline at end of file +# eslint-plugin-import의 no-unused-modules 규칙이 FileEnumerator 내부 API를 사용할 때 +# .eslintignore 파일의 존재를 필요로 합니다. 내용이 없어도 파일이 있어야 정상 동작합니다. diff --git a/apps/crew/.eslintrc.json b/apps/crew/.eslintrc.json index 4f2b100b4..382ebeeda 100644 --- a/apps/crew/.eslintrc.json +++ b/apps/crew/.eslintrc.json @@ -1,38 +1,4 @@ { "root": true, - "parser": "@typescript-eslint/parser", - "env": { - "es6": true, - "node": true, - "browser": true - }, - "plugins": ["@typescript-eslint", "prettier", "import"], - "extends": [ - "eslint:recommended", - "plugin:prettier/recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - "plugin:storybook/recommended" - ], - "rules": { - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-empty-function": "off", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "react-hooks/set-state-in-effect": "warn", - "react-hooks/static-components": "warn", - "react-hooks/immutability": "warn", - "react-hooks/refs": "warn", - "react-hooks/incompatible-library": "warn", - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ], - "import/no-unused-modules": [1, { "unusedExports": true }], - "react/no-unstable-nested-components": "off" - } + "extends": ["@sopt-makers/eslint-config/next"] } diff --git a/apps/crew/.gitignore b/apps/crew/.gitignore deleted file mode 100644 index f1c8bc791..000000000 --- a/apps/crew/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# vscode -.vscode/ -.idea - -# __generated__ -src/__generated__/open-api-specification.json - -# 최적화 관련 파일이기 때문에 애초에 커밋할 필요 없음 -install-state.gz diff --git a/apps/crew/.prettierrc b/apps/crew/.prettierrc deleted file mode 100644 index c7d04dcd9..000000000 --- a/apps/crew/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": true, - "singleQuote": true, - "arrowParens": "avoid", - "printWidth": 120 -} diff --git a/apps/crew/.storybook/main.ts b/apps/crew/.storybook/main.ts index 34d024b08..8422354ad 100644 --- a/apps/crew/.storybook/main.ts +++ b/apps/crew/.storybook/main.ts @@ -11,7 +11,7 @@ const config: StorybookConfig = { autodocs: 'tag', }, webpackFinal(config) { - const imageRule = config.module!.rules!.find(rule => { + const imageRule = config.module!.rules!.find((rule) => { if (rule && typeof rule !== 'string' && rule.test instanceof RegExp) { return rule.test.test('.svg'); } diff --git a/apps/crew/.storybook/preview.tsx b/apps/crew/.storybook/preview.tsx index 5fd694263..5c5b440c2 100644 --- a/apps/crew/.storybook/preview.tsx +++ b/apps/crew/.storybook/preview.tsx @@ -1,5 +1,5 @@ import type { Preview } from '@storybook/react'; -import { OverlayProvider } from '../src/hooks/useOverlay/OverlayProvider'; +import { OverlayProvider } from '../src/hook/useOverlay/OverlayProvider'; import React from 'react'; import '../styles/globals.css'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -24,7 +24,7 @@ const preview: Preview = { }, }, decorators: [ - Story => { + (Story) => { const queryClient = new QueryClient(); return ( diff --git a/apps/crew/ampli.json b/apps/crew/ampli.json index 2d54d6d63..ad3b3acfe 100644 --- a/apps/crew/ampli.json +++ b/apps/crew/ampli.json @@ -11,4 +11,4 @@ "Language": "TypeScript", "SDK": "@amplitude/analytics-browser@^1.0", "Path": "./src/ampli" -} \ No newline at end of file +} diff --git a/apps/crew/next.config.js b/apps/crew/next.config.js index 5772047b3..f46818f26 100644 --- a/apps/crew/next.config.js +++ b/apps/crew/next.config.js @@ -5,7 +5,7 @@ const nextConfig = { output: 'export', reactStrictMode: true, swcMinify: true, - webpack: config => { + webpack: (config) => { config.module.rules.push({ test: /\.svg$/, oneOf: [ diff --git a/apps/crew/package.json b/apps/crew/package.json index f824b06ce..95d479182 100644 --- a/apps/crew/package.json +++ b/apps/crew/package.json @@ -3,18 +3,19 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", "build": "next build", - "start": "next start", - "lint": "next lint", - "typecheck": "tsc --noemit -p .", - "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", + "dev": "next dev", + "format": "prettier --write . --ignore-path ../../.prettierignore", + "generate-types": "cat ./src/__generated__/schema1.d.ts ./src/__generated__/schema2.d.ts > ./src/__generated__/schema.d.ts", "generate-types-v1": "openapi-typescript https://crew.api.dev.sopt.org/api-docs-json -o ./src/__generated__/schema1.d.ts", "generate-types-v2": "openapi-typescript https://crew.api.dev.sopt.org/api-docs/json -o ./src/__generated__/schema2.d.ts", - "generate-types": "cat ./src/__generated__/schema1.d.ts ./src/__generated__/schema2.d.ts > ./src/__generated__/schema.d.ts", - "wrangler": "wrangler", - "where": "sh -c 'export IP=`ipconfig getifaddr en0` && echo [info] we are in ... http://${IP}:3000'" + "lint": "next lint", + "start": "next start", + "storybook": "storybook dev -p 6006", + "typecheck": "tsc --noemit -p .", + "where": "sh -c 'export IP=`ipconfig getifaddr en0` && echo [info] we are in ... http://${IP}:3000'", + "wrangler": "wrangler" }, "dependencies": { "@amplitude/ampli": "^1.35.0", @@ -52,12 +53,7 @@ "@hookform/devtools": "^4.3.1", "@types/react-mentions": "4.1.13", "@types/react-responsive": "^8.0.5", - "eslint": "^8.57.0", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-prettier": "^5.5.5", - "eslint-plugin-react-hooks": "^7.0.1", "openapi-typescript": "^6.3.9", - "prettier": "^3.0.0", "wrangler": "^3.0.0" } } diff --git a/apps/crew/packages/react-mentions/src/Highlighter.js b/apps/crew/packages/react-mentions/src/Highlighter.js index 3a51fe190..3bc5555bb 100644 --- a/apps/crew/packages/react-mentions/src/Highlighter.js +++ b/apps/crew/packages/react-mentions/src/Highlighter.js @@ -1,22 +1,17 @@ -import React, { Children, useState, useEffect } from 'react' -import PropTypes from 'prop-types' -import { defaultStyle } from './utils' +import React, { Children, useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { defaultStyle } from './utils'; -import { - iterateMentionsMarkup, - mapPlainTextIndex, - readConfigFromChildren, - isNumber, -} from './utils' +import { iterateMentionsMarkup, mapPlainTextIndex, readConfigFromChildren, isNumber } from './utils'; const _generateComponentKey = (usedKeys, id) => { if (!usedKeys.hasOwnProperty(id)) { - usedKeys[id] = 0 + usedKeys[id] = 0; } else { - usedKeys[id]++ + usedKeys[id]++; } - return id + '_' + usedKeys[id] -} + return id + '_' + usedKeys[id]; +}; function Highlighter({ selectionStart, @@ -31,46 +26,41 @@ function Highlighter({ const [position, setPosition] = useState({ left: undefined, top: undefined, - }) - const [caretElement, setCaretElement] = useState() + }); + const [caretElement, setCaretElement] = useState(); useEffect(() => { - notifyCaretPosition() - }) + notifyCaretPosition(); + }); const notifyCaretPosition = () => { if (!caretElement) { - return + return; } - const { offsetLeft, offsetTop } = caretElement + const { offsetLeft, offsetTop } = caretElement; if (position.left === offsetLeft && position.top === offsetTop) { - return + return; } - const newPosition = { left: offsetLeft, top: offsetTop } - setPosition(newPosition) + const newPosition = { left: offsetLeft, top: offsetTop }; + setPosition(newPosition); - onCaretPositionChange(newPosition) - } + onCaretPositionChange(newPosition); + }; - const config = readConfigFromChildren(children) - let caretPositionInMarkup + const config = readConfigFromChildren(children); + let caretPositionInMarkup; if (selectionEnd === selectionStart) { - caretPositionInMarkup = mapPlainTextIndex( - value, - config, - selectionStart, - 'START' - ) + caretPositionInMarkup = mapPlainTextIndex(value, config, selectionStart, 'START'); } - const resultComponents = [] - const componentKeys = {} - let components = resultComponents - let substringComponentKey = 0 + const resultComponents = []; + const componentKeys = {}; + let components = resultComponents; + let substringComponentKey = 0; const textIteratee = (substr, index, indexInPlainText) => { // check whether the caret element has to be inserted inside the current plain substring @@ -80,35 +70,21 @@ function Highlighter({ caretPositionInMarkup <= index + substr.length ) { // if yes, split substr at the caret position and insert the caret component - const splitIndex = caretPositionInMarkup - index - components.push( - renderSubstring(substr.substring(0, splitIndex), substringComponentKey) - ) + const splitIndex = caretPositionInMarkup - index; + components.push(renderSubstring(substr.substring(0, splitIndex), substringComponentKey)); // add all following substrings and mention components as children of the caret component - components = [ - renderSubstring(substr.substring(splitIndex), substringComponentKey), - ] + components = [renderSubstring(substr.substring(splitIndex), substringComponentKey)]; } else { - components.push(renderSubstring(substr, substringComponentKey)) + components.push(renderSubstring(substr, substringComponentKey)); } - substringComponentKey++ - } + substringComponentKey++; + }; - const mentionIteratee = ( - markup, - index, - indexInPlainText, - id, - display, - mentionChildIndex, - lastMentionEndIndex - ) => { - const key = _generateComponentKey(componentKeys, id) - components.push( - getMentionComponentForMatch(id, display, mentionChildIndex, key) - ) - } + const mentionIteratee = (markup, index, indexInPlainText, id, display, mentionChildIndex, lastMentionEndIndex) => { + const key = _generateComponentKey(componentKeys, id); + components.push(getMentionComponentForMatch(id, display, mentionChildIndex, key)); + }; const renderSubstring = (string, key) => { // set substring span to hidden, so that Emojis are not shown double in Mobile Safari @@ -116,38 +92,38 @@ function Highlighter({ {string} - ) - } + ); + }; const getMentionComponentForMatch = (id, display, mentionChildIndex, key) => { - const props = { id, display, key } - const child = Children.toArray(children)[mentionChildIndex] - return React.cloneElement(child, props) - } + const props = { id, display, key }; + const child = Children.toArray(children)[mentionChildIndex]; + return React.cloneElement(child, props); + }; const renderHighlighterCaret = (children) => { return ( - + {children} - ) - } + ); + }; - iterateMentionsMarkup(value, config, mentionIteratee, textIteratee) + iterateMentionsMarkup(value, config, mentionIteratee, textIteratee); // append a span containing a space, to ensure the last text line has the correct height - components.push(' ') + components.push(' '); if (components !== resultComponents) { // if a caret component is to be rendered, add all components that followed as its children - resultComponents.push(renderHighlighterCaret(components)) + resultComponents.push(renderHighlighterCaret(components)); } return (
{resultComponents}
- ) + ); } Highlighter.propTypes = { @@ -158,42 +134,36 @@ Highlighter.propTypes = { containerRef: PropTypes.oneOfType([ PropTypes.func, PropTypes.shape({ - current: - typeof Element === 'undefined' - ? PropTypes.any - : PropTypes.instanceOf(Element), + current: typeof Element === 'undefined' ? PropTypes.any : PropTypes.instanceOf(Element), }), ]), - children: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.arrayOf(PropTypes.element), - ]).isRequired, -} + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]).isRequired, +}; const styled = defaultStyle( { - position: 'relative', - boxSizing: 'border-box', - width: '100%', - color: 'transparent', - overflow: 'hidden', - whiteSpace: 'pre-wrap', - wordWrap: 'break-word', - border: '1px solid transparent', - textAlign: 'start', + 'position': 'relative', + 'boxSizing': 'border-box', + 'width': '100%', + 'color': 'transparent', + 'overflow': 'hidden', + 'whiteSpace': 'pre-wrap', + 'wordWrap': 'break-word', + 'border': '1px solid transparent', + 'textAlign': 'start', '&singleLine': { whiteSpace: 'pre', wordWrap: null, }, - substring: { + 'substring': { visibility: 'hidden', }, }, (props) => ({ '&singleLine': props.singleLine, - }) -) + }), +); -export default styled(Highlighter) +export default styled(Highlighter); diff --git a/apps/crew/packages/react-mentions/src/Highlighter.spec.js b/apps/crew/packages/react-mentions/src/Highlighter.spec.js index 06ef762db..a8ae12268 100644 --- a/apps/crew/packages/react-mentions/src/Highlighter.spec.js +++ b/apps/crew/packages/react-mentions/src/Highlighter.spec.js @@ -1,7 +1,7 @@ describe('Highlighter', () => { - it.todo('should notify about the current caret position when mounted.') - it.todo('should notify about the current care position whenever it changes.') - it.todo('should render the current matched mentions.') - it.todo('should only show the matched mentions.') - it.todo('should be possible to style the mentions.') -}) + it.todo('should notify about the current caret position when mounted.'); + it.todo('should notify about the current care position whenever it changes.'); + it.todo('should render the current matched mentions.'); + it.todo('should only show the matched mentions.'); + it.todo('should be possible to style the mentions.'); +}); diff --git a/apps/crew/packages/react-mentions/src/LoadingIndicator.js b/apps/crew/packages/react-mentions/src/LoadingIndicator.js index ed9a42aa0..ab94b3519 100644 --- a/apps/crew/packages/react-mentions/src/LoadingIndicator.js +++ b/apps/crew/packages/react-mentions/src/LoadingIndicator.js @@ -1,9 +1,9 @@ -import React from 'react' -import useStyles from 'substyle' +import React from 'react'; +import useStyles from 'substyle'; function LoadingIndicator({ style, className, classNames }) { - const styles = useStyles(defaultstyle, { style, className, classNames }) - const spinnerStyles = styles('spinner') + const styles = useStyles(defaultstyle, { style, className, classNames }); + const spinnerStyles = styles('spinner'); return (
@@ -14,9 +14,9 @@ function LoadingIndicator({ style, className, classNames }) {
- ) + ); } -const defaultstyle = {} +const defaultstyle = {}; -export default LoadingIndicator +export default LoadingIndicator; diff --git a/apps/crew/packages/react-mentions/src/Mention.js b/apps/crew/packages/react-mentions/src/Mention.js index 36bdc6875..445d10ca3 100644 --- a/apps/crew/packages/react-mentions/src/Mention.js +++ b/apps/crew/packages/react-mentions/src/Mention.js @@ -1,15 +1,15 @@ -import React from 'react' -import PropTypes from 'prop-types' -import useStyles from 'substyle' +import React from 'react'; +import PropTypes from 'prop-types'; +import useStyles from 'substyle'; const defaultStyle = { fontWeight: 'inherit', -} +}; const Mention = ({ display, style, className, classNames }) => { - const styles = useStyles(defaultStyle, { style, className, classNames }) - return {display} -} + const styles = useStyles(defaultStyle, { style, className, classNames }); + return {display}; +}; Mention.propTypes = { /** @@ -28,10 +28,7 @@ Mention.propTypes = { renderSuggestion: PropTypes.func, - trigger: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(RegExp), - ]), + trigger: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(RegExp)]), markup: PropTypes.string, displayTransform: PropTypes.func, /** @@ -40,19 +37,19 @@ Mention.propTypes = { allowSpaceInQuery: PropTypes.bool, isLoading: PropTypes.bool, -} +}; Mention.defaultProps = { trigger: '@', markup: '@[__display__](__id__)', - displayTransform: function(id, display) { - return display || id + displayTransform: function (id, display) { + return display || id; }, onAdd: () => null, onRemove: () => null, renderSuggestion: null, isLoading: false, appendSpaceOnAdd: false, -} +}; -export default Mention +export default Mention; diff --git a/apps/crew/packages/react-mentions/src/MentionsInput.js b/apps/crew/packages/react-mentions/src/MentionsInput.js index 124999b0a..69047f335 100755 --- a/apps/crew/packages/react-mentions/src/MentionsInput.js +++ b/apps/crew/packages/react-mentions/src/MentionsInput.js @@ -1,4 +1,4 @@ -import React, { Children } from 'react' +import React, { Children } from 'react'; import { applyChangeToValue, countSuggestions, @@ -17,53 +17,49 @@ import { keys, omit, getSuggestionHtmlId, -} from './utils' +} from './utils'; -import Highlighter from './Highlighter' -import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import SuggestionsOverlay from './SuggestionsOverlay' -import { defaultStyle } from './utils' +import Highlighter from './Highlighter'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import SuggestionsOverlay from './SuggestionsOverlay'; +import { defaultStyle } from './utils'; -export const makeTriggerRegex = function(trigger, options = {}) { +export const makeTriggerRegex = function (trigger, options = {}) { if (trigger instanceof RegExp) { - return trigger + return trigger; } else { - const { allowSpaceInQuery } = options - const escapedTriggerChar = escapeRegex(trigger) + const { allowSpaceInQuery } = options; + const escapedTriggerChar = escapeRegex(trigger); // first capture group is the part to be replaced on completion // second capture group is for extracting the search query - return new RegExp( - `(?:^|\\s)(${escapedTriggerChar}([^${ - allowSpaceInQuery ? '' : '\\s' - }${escapedTriggerChar}]*))$` - ) + return new RegExp(`(?:^|\\s)(${escapedTriggerChar}([^${allowSpaceInQuery ? '' : '\\s'}${escapedTriggerChar}]*))$`); } -} +}; -const getDataProvider = function(data, ignoreAccents) { +const getDataProvider = function (data, ignoreAccents) { if (data instanceof Array) { // if data is an array, create a function to query that - return function(query, callback) { - const results = [] + return function (query, callback) { + const results = []; for (let i = 0, l = data.length; i < l; ++i) { - const display = data[i].display || data[i].id + const display = data[i].display || data[i].id; if (getSubstringIndex(display, query, ignoreAccents) >= 0) { - results.push(data[i]) + results.push(data[i]); } } - return results - } + return results; + }; } else { // expect data to be a query function - return data + return data; } -} +}; -const KEY = { TAB: 9, RETURN: 13, ESC: 27, UP: 38, DOWN: 40 } +const KEY = { TAB: 9, RETURN: 13, ESC: 27, UP: 38, DOWN: 40 }; -let isComposing = false +let isComposing = false; const propTypes = { /** @@ -83,28 +79,19 @@ const propTypes = { onSelect: PropTypes.func, onBlur: PropTypes.func, onChange: PropTypes.func, - suggestionsPortalHost: - typeof Element === 'undefined' - ? PropTypes.any - : PropTypes.PropTypes.instanceOf(Element), + suggestionsPortalHost: typeof Element === 'undefined' ? PropTypes.any : PropTypes.PropTypes.instanceOf(Element), inputRef: PropTypes.oneOfType([ PropTypes.func, PropTypes.shape({ - current: - typeof Element === 'undefined' - ? PropTypes.any - : PropTypes.instanceOf(Element), + current: typeof Element === 'undefined' ? PropTypes.any : PropTypes.instanceOf(Element), }), ]), - children: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.arrayOf(PropTypes.element), - ]).isRequired, -} + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]).isRequired, +}; class MentionsInput extends React.Component { - static propTypes = propTypes + static propTypes = propTypes; static defaultProps = { ignoreAccents: false, @@ -113,18 +100,16 @@ class MentionsInput extends React.Component { onKeyDown: () => null, onSelect: () => null, onBlur: () => null, - } + }; constructor(props) { - super(props) - this.suggestions = {} - this.uuidSuggestionsOverlay = Math.random() - .toString(16) - .substring(2) + super(props); + this.suggestions = {}; + this.uuidSuggestionsOverlay = Math.random().toString(16).substring(2); - this.handleCopy = this.handleCopy.bind(this) - this.handleCut = this.handleCut.bind(this) - this.handlePaste = this.handlePaste.bind(this) + this.handleCopy = this.handleCopy.bind(this); + this.handleCut = this.handleCut.bind(this); + this.handlePaste = this.handlePaste.bind(this); this.state = { focusIndex: 0, @@ -138,40 +123,40 @@ class MentionsInput extends React.Component { suggestionsPosition: {}, setSelectionAfterHandlePaste: false, - } + }; } componentDidMount() { - document.addEventListener('copy', this.handleCopy) - document.addEventListener('cut', this.handleCut) - document.addEventListener('paste', this.handlePaste) + document.addEventListener('copy', this.handleCopy); + document.addEventListener('cut', this.handleCut); + document.addEventListener('paste', this.handlePaste); - this.updateSuggestionsPosition() + this.updateSuggestionsPosition(); } componentDidUpdate(prevProps, prevState) { // Update position of suggestions unless this componentDidUpdate was // triggered by an update to suggestionsPosition. if (prevState.suggestionsPosition === this.state.suggestionsPosition) { - this.updateSuggestionsPosition() + this.updateSuggestionsPosition(); } // maintain selection in case a mention is added/removed causing // the cursor to jump to the end if (this.state.setSelectionAfterMentionChange) { - this.setState({ setSelectionAfterMentionChange: false }) - this.setSelection(this.state.selectionStart, this.state.selectionEnd) + this.setState({ setSelectionAfterMentionChange: false }); + this.setSelection(this.state.selectionStart, this.state.selectionEnd); } if (this.state.setSelectionAfterHandlePaste) { - this.setState({ setSelectionAfterHandlePaste: false }) - this.setSelection(this.state.selectionStart, this.state.selectionEnd) + this.setState({ setSelectionAfterHandlePaste: false }); + this.setSelection(this.state.selectionStart, this.state.selectionEnd); } } componentWillUnmount() { - document.removeEventListener('copy', this.handleCopy) - document.removeEventListener('cut', this.handleCut) - document.removeEventListener('paste', this.handlePaste) + document.removeEventListener('copy', this.handleCopy); + document.removeEventListener('cut', this.handleCut); + document.removeEventListener('paste', this.handlePaste); } render() { @@ -180,22 +165,22 @@ class MentionsInput extends React.Component { {this.renderControl()} {this.renderSuggestionsOverlay()}
- ) + ); } setContainerElement = (el) => { - this.containerElement = el - } + this.containerElement = el; + }; getInputProps = () => { - let { readOnly, disabled, style } = this.props + let { readOnly, disabled, style } = this.props; // pass all props that neither we, nor substyle, consume through to the input control let props = omit( this.props, ['style', 'classNames', 'className'], // substyle props - keys(propTypes) - ) + keys(propTypes), + ); return { ...props, @@ -215,63 +200,58 @@ class MentionsInput extends React.Component { }), ...(this.isOpened() && { - role: 'combobox', + 'role': 'combobox', 'aria-controls': this.uuidSuggestionsOverlay, 'aria-expanded': true, 'aria-autocomplete': 'list', 'aria-haspopup': 'listbox', - 'aria-activedescendant': getSuggestionHtmlId( - this.uuidSuggestionsOverlay, - this.state.focusIndex - ), + 'aria-activedescendant': getSuggestionHtmlId(this.uuidSuggestionsOverlay, this.state.focusIndex), }), - } - } + }; + }; renderControl = () => { - let { singleLine, style } = this.props - let inputProps = this.getInputProps() + let { singleLine, style } = this.props; + let inputProps = this.getInputProps(); return (
{this.renderHighlighter()} - {singleLine - ? this.renderInput(inputProps) - : this.renderTextarea(inputProps)} + {singleLine ? this.renderInput(inputProps) : this.renderTextarea(inputProps)}
- ) - } + ); + }; renderInput = (props) => { - return - } + return ; + }; renderTextarea = (props) => { - return