Repository: hackjutsu/Lepton Branch: master Commit: a1b2c4f0ec26 Files: 201 Total size: 1.4 MB Directory structure: gitextract_qzfayoof/ ├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CLAUDE.md ├── LICENSE ├── README.md ├── app/ │ ├── actions/ │ │ └── index.js │ ├── containers/ │ │ ├── aboutPage/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── appContainer/ │ │ │ ├── index.js │ │ │ ├── index.scss │ │ │ └── scrollbar.scss │ │ ├── codeArea/ │ │ │ ├── index.js │ │ │ ├── jupyterNotebook.scss │ │ │ └── markdown.scss │ │ ├── dashboard/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── gistEditor/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── gistEditorForm/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── loginPage/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── navigationPanel/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── navigationPanelDetails/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── searchPage/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── snippet/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── snippetPanel/ │ │ │ ├── index.js │ │ │ └── index.scss │ │ └── userPanel/ │ │ ├── index.js │ │ └── index.scss │ ├── index.js │ ├── reducers/ │ │ ├── index.js │ │ ├── reducer_about_modal.js │ │ ├── reducer_active_gist.js │ │ ├── reducer_active_gist_tag.js │ │ ├── reducer_auth_window_status.js │ │ ├── reducer_dashboard_modal.js │ │ ├── reducer_file_expand_status.js │ │ ├── reducer_gist_delete_modal.js │ │ ├── reducer_gist_edit_modal.js │ │ ├── reducer_gist_new_modal.js │ │ ├── reducer_gist_raw_modal.js │ │ ├── reducer_gist_sync_status.js │ │ ├── reducer_gist_tags.js │ │ ├── reducer_gists.js │ │ ├── reducer_immersive_mode.js │ │ ├── reducer_logout_modal.js │ │ ├── reducer_new_version_info.js │ │ ├── reducer_pinned_tags.js │ │ ├── reducer_pinned_tags_selections_modal.js │ │ ├── reducer_scroll_request_status.js │ │ ├── reducer_search_window_status.js │ │ ├── reducer_sync_time.js │ │ ├── reducer_token.js │ │ ├── reducer_update_available_bar_status.js │ │ └── reducer_user_session.js │ └── utilities/ │ ├── githubApi/ │ │ └── index.js │ ├── jupyterNotebook/ │ │ └── index.js │ ├── markdown/ │ │ └── index.js │ ├── menu/ │ │ └── mainMenu.js │ ├── notifier/ │ │ └── index.js │ ├── parser/ │ │ └── index.js │ ├── search/ │ │ └── index.js │ ├── store/ │ │ └── index.js │ ├── themeManager/ │ │ ├── index.js │ │ └── themes/ │ │ ├── darkTheme.json │ │ └── lightTheme.json │ └── vendor/ │ ├── bootstrap/ │ │ └── css/ │ │ ├── bootstrap-theme.css │ │ └── bootstrap.css │ ├── highlightJS/ │ │ └── styles/ │ │ ├── agate.css │ │ ├── androidstudio.css │ │ ├── arduino-light.css │ │ ├── arta.css │ │ ├── ascetic.css │ │ ├── atelier-cave-dark.css │ │ ├── atelier-cave-light.css │ │ ├── atelier-dune-dark.css │ │ ├── atelier-dune-light.css │ │ ├── atelier-estuary-dark.css │ │ ├── atelier-estuary-light.css │ │ ├── atelier-forest-dark.css │ │ ├── atelier-forest-light.css │ │ ├── atelier-heath-dark.css │ │ ├── atelier-heath-light.css │ │ ├── atelier-lakeside-dark.css │ │ ├── atelier-lakeside-light.css │ │ ├── atelier-plateau-dark.css │ │ ├── atelier-plateau-light.css │ │ ├── atelier-savanna-dark.css │ │ ├── atelier-savanna-light.css │ │ ├── atelier-seaside-dark.css │ │ ├── atelier-seaside-light.css │ │ ├── atelier-sulphurpool-dark.css │ │ ├── atelier-sulphurpool-light.css │ │ ├── atom-one-dark.css │ │ ├── atom-one-light.css │ │ ├── brown-paper.css │ │ ├── codepen-embed.css │ │ ├── color-brewer.css │ │ ├── darcula.css │ │ ├── dark.css │ │ ├── darkula.css │ │ ├── default.css │ │ ├── docco.css │ │ ├── dracula.css │ │ ├── far.css │ │ ├── foundation.css │ │ ├── github-gist.css │ │ ├── github.css │ │ ├── googlecode.css │ │ ├── grayscale.css │ │ ├── gruvbox-dark.css │ │ ├── gruvbox-light.css │ │ ├── hopscotch.css │ │ ├── hybrid.css │ │ ├── idea.css │ │ ├── ir-black.css │ │ ├── kimbie.dark.css │ │ ├── kimbie.light.css │ │ ├── magula.css │ │ ├── mono-blue.css │ │ ├── monokai-sublime.css │ │ ├── monokai.css │ │ ├── obsidian.css │ │ ├── ocean.css │ │ ├── paraiso-dark.css │ │ ├── paraiso-light.css │ │ ├── pojoaque.css │ │ ├── purebasic.css │ │ ├── qtcreator_dark.css │ │ ├── qtcreator_light.css │ │ ├── railscasts.css │ │ ├── rainbow.css │ │ ├── school-book.css │ │ ├── solarized-dark.css │ │ ├── solarized-light.css │ │ ├── sunburst.css │ │ ├── tomorrow-night-blue.css │ │ ├── tomorrow-night-bright.css │ │ ├── tomorrow-night-eighties.css │ │ ├── tomorrow-night.css │ │ ├── tomorrow.css │ │ ├── vs.css │ │ ├── xcode.css │ │ ├── xt256.css │ │ └── zenburn.css │ └── prism/ │ └── prism.scss ├── build/ │ └── icon.icns ├── configs/ │ ├── accountDummy.js │ └── defaultConfig.js ├── docs/ │ ├── css/ │ │ └── freelancer.css │ ├── index.html │ ├── js/ │ │ ├── contact_me.js │ │ ├── freelancer.js │ │ └── jqBootstrapValidation.js │ └── vendor/ │ ├── bootstrap/ │ │ ├── css/ │ │ │ └── bootstrap.css │ │ └── js/ │ │ └── bootstrap.js │ ├── font-awesome/ │ │ ├── css/ │ │ │ └── font-awesome.css │ │ ├── fonts/ │ │ │ └── FontAwesome.otf │ │ ├── less/ │ │ │ ├── animated.less │ │ │ ├── bordered-pulled.less │ │ │ ├── core.less │ │ │ ├── fixed-width.less │ │ │ ├── font-awesome.less │ │ │ ├── icons.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── mixins.less │ │ │ ├── path.less │ │ │ ├── rotated-flipped.less │ │ │ ├── screen-reader.less │ │ │ ├── stacked.less │ │ │ └── variables.less │ │ └── scss/ │ │ ├── _animated.scss │ │ ├── _bordered-pulled.scss │ │ ├── _core.scss │ │ ├── _fixed-width.scss │ │ ├── _icons.scss │ │ ├── _larger.scss │ │ ├── _list.scss │ │ ├── _mixins.scss │ │ ├── _path.scss │ │ ├── _rotated-flipped.scss │ │ ├── _screen-reader.scss │ │ ├── _stacked.scss │ │ ├── _variables.scss │ │ └── font-awesome.scss │ └── jquery/ │ └── jquery.js ├── index.html ├── license.json ├── main.js ├── package.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "files": [ "README.md" ], "imageSize": 100, "commit": false, "contributors": [ { "login": "hackjutsu", "name": "CosmoX", "avatar_url": "https://avatars3.githubusercontent.com/u/7756581?v=4", "profile": "https://airbnb.io/", "contributions": [ "code", "design", "test", "maintenance", "platform", "ideas" ] }, { "login": "wujysh", "name": "Jiaye Wu", "avatar_url": "https://avatars1.githubusercontent.com/u/5550402?v=4", "profile": "https://loveac.cn", "contributions": [ "code", "maintenance", "ideas" ] }, { "login": "DNLHC", "name": "Danila", "avatar_url": "https://avatars1.githubusercontent.com/u/14959483?v=4", "profile": "https://github.com/DNLHC", "contributions": [ "code", "design", "maintenance" ] }, { "login": "meilinz", "name": "Meilin Zhan", "avatar_url": "https://avatars2.githubusercontent.com/u/13786673?v=4", "profile": "http://www.meilinzhan.com/", "contributions": [ "code", "ideas", "maintenance" ] }, { "login": "lcgforever", "name": "lcgforever", "avatar_url": "https://avatars1.githubusercontent.com/u/5697293?v=4", "profile": "http://www.linkedin.com/in/liuchenguang", "contributions": [ "code" ] }, { "login": "passerbyid", "name": "Yuer Lee", "avatar_url": "https://avatars1.githubusercontent.com/u/2075566?v=4", "profile": "https://github.com/passerbyid", "contributions": [ "doc", "platform" ] }, { "login": "YYSU", "name": "Su,Yen-Yun", "avatar_url": "https://avatars3.githubusercontent.com/u/12994810?v=4", "profile": "http://yysu.github.io/About-me", "contributions": [ "doc" ] }, { "login": "cixuuz", "name": "Chen Tong", "avatar_url": "https://avatars3.githubusercontent.com/u/26782336?v=4", "profile": "https://cixuuz.github.io/", "contributions": [ "code", "ideas", "maintenance" ] }, { "login": "Gisonrg", "name": "Jason Jiang", "avatar_url": "https://avatars0.githubusercontent.com/u/4332224?v=4", "profile": "https://github.com/Gisonrg", "contributions": [ "code" ] }, { "login": "popey", "name": "Alan Pope", "avatar_url": "https://avatars0.githubusercontent.com/u/1841272?v=4", "profile": "http://popey.com/", "contributions": [ "platform" ] }, { "login": "tonyxu-io", "name": "Tony Xu", "avatar_url": "https://avatars3.githubusercontent.com/u/6280136?v=4", "profile": "https://tonyxu.io", "contributions": [ "platform" ] }, { "login": "rawrmonstar", "name": "Tegan Churchill", "avatar_url": "https://avatars0.githubusercontent.com/u/13814048?v=4", "profile": "https://tegan.lol", "contributions": [ "code" ] }, { "login": "AngieW0908", "name": "Angie Wang", "avatar_url": "https://avatars3.githubusercontent.com/u/26016229?v=4", "profile": "https://github.com/AngieW0908", "contributions": [ "design" ] }, { "login": "baybatu", "name": "Batuhan Bayrakci", "avatar_url": "https://avatars0.githubusercontent.com/u/965804?v=4", "profile": "http://batuhanbayrakci.com", "contributions": [ "code" ] }, { "login": "samuelmeuli", "name": "Samuel Meuli", "avatar_url": "https://avatars0.githubusercontent.com/u/22477950?v=4", "profile": "https://samuelmeuli.com", "contributions": [ "code" ] }, { "login": "alexandreamadocastro", "name": "Alexandre Amado de Castro", "avatar_url": "https://avatars2.githubusercontent.com/u/5918765?v=4", "profile": "https://www.linkedin.com/in/alexandreamadocastro", "contributions": [ "code" ] }, { "login": "abnersajr", "name": "Abner Soares Alves Junior", "avatar_url": "https://avatars2.githubusercontent.com/u/1998649?v=4", "profile": "http://abner.space/", "contributions": [ "code" ] }, { "login": "seancheung", "name": "Sean", "avatar_url": "https://avatars0.githubusercontent.com/u/5442563?v=4", "profile": "http://seanz.me", "contributions": [ "code" ] }, { "login": "moia-sven-ole", "name": "Ole", "avatar_url": "https://avatars0.githubusercontent.com/u/32508538?v=4", "profile": "https://github.com/moia-sven-ole", "contributions": [ "code" ] }, { "login": "GabrielNicolasAvellaneda", "name": "Gabriel Nicolas Avellaneda", "avatar_url": "https://avatars3.githubusercontent.com/u/1248101?v=4", "profile": "https://www.linkedin.com/in/GabrielNicolasAvellaneda/", "contributions": [ "code", "doc" ] }, { "login": "dideler", "name": "Dennis Ideler", "avatar_url": "https://avatars2.githubusercontent.com/u/497458?v=4", "profile": "https://dideler.github.io", "contributions": [ "code", "ideas", "doc" ] }, { "login": "anthonyattard", "name": "Anthony Attard", "avatar_url": "https://avatars0.githubusercontent.com/u/8838135?v=4", "profile": "http://AnthonyAttard.com", "contributions": [ "code" ] }, { "login": "ArLEquiN64", "name": "ArLE", "avatar_url": "https://avatars1.githubusercontent.com/u/7821318?v=4", "profile": "https://ArLEquiN64.github.io/", "contributions": [ "code" ] }, { "login": "polnetwork", "name": "Pol Maresma", "avatar_url": "https://avatars1.githubusercontent.com/u/639877?v=4", "profile": "http://www.polnetwork.com", "contributions": [ "code" ] }, { "login": "PMExtra", "name": "PM Extra", "avatar_url": "https://avatars.githubusercontent.com/u/11289158?v=4", "profile": "https://blog.jubeat.net", "contributions": [ "code" ] }, { "login": "EdZava", "name": "Zava", "avatar_url": "https://avatars.githubusercontent.com/u/1155199?v=4", "profile": "https://zava.carrd.co/", "contributions": [ "code" ] }, { "login": "sunnysidesounds", "name": "Jason R Alexander", "avatar_url": "https://avatars.githubusercontent.com/u/1030838?v=4", "profile": "http://www.linkedin.com/in/jasonralexander", "contributions": [ "code" ] }, { "login": "Sebastian-Hojas", "name": "Sebastian Hojas", "avatar_url": "https://avatars.githubusercontent.com/u/279378?v=4", "profile": "http://irrelevant.at", "contributions": [ "doc" ] }, { "login": "yuhang-dong", "name": "董雨航", "avatar_url": "https://avatars.githubusercontent.com/u/20642641?v=4", "profile": "https://github.com/yuhang-dong", "contributions": [ "code" ] }, { "login": "sxyazi", "name": "sxyazi", "avatar_url": "https://avatars.githubusercontent.com/u/17523360?v=4", "profile": "https://sxyz.blog", "contributions": [ "platform" ] }, { "login": "ProfessorManhattan", "name": "Brian Zalewski", "avatar_url": "https://avatars.githubusercontent.com/u/59970525?v=4", "profile": "https://megabyte.space", "contributions": [ "platform" ] } ], "contributorsPerLine": 7, "projectName": "Lepton", "projectOwner": "hackjutsu", "repoType": "github", "repoHost": "https://github.com", "skipCi": true, "commitType": "docs", "commitConvention": "angular" } ================================================ FILE: .eslintignore ================================================ dist **/node_modules **/webpack*.config.js ================================================ FILE: .eslintrc.js ================================================ module.exports = { "extends": "standard", "env": { "browser": true, "mocha": true, "node": true }, "rules": { "indent": [2, 2, {"SwitchCase": 1}], "no-new": 0, "react/jsx-uses-react": 2, "react/jsx-uses-vars": 2, "react/react-in-jsx-scope": 2, "react/sort-comp": 0, "react/no-multi-comp": 0, "comma-dangle": 0, "id-length": 0, "new-cap": 0, "eol-last": 0, "jsx-quotes": 0, "consistent-return": 0, "import/no-named-default": 0, }, "parser": "babel-eslint", "plugins": [ "standard", "react" ], }; ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ The more we know about your system and use case, the more easily and likely we can help. #### Environment info * OS: * App Version (`Command/Ctrl + ,`): #### Description of the problem / feature request / question: #### If possible, provide a sample gist or screenshot: #### If possible, provide the log files For Lepton after v1.9.0, the path of the log file can be found at `Command/Ctrl + ,` - Mac: `~/Library/Application Support/Lepton/logs` - Windows: `C:\Users\\AppData\Roaming\Lepton\logs` - Linux: `~/.cached/Lepton/logs` >The [debug mode](https://github.com/hackjutsu/Lepton/wiki) is recommended when producing the logs. >You can send your log file to cosmo.lepton@gmail.com if don't want to post it here. ================================================ FILE: .gitignore ================================================ node_modules bundle dist .DS_Store configs/account.js npm-debug.log test screenshots *backup* .idea .vscode ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "14" ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Lepton is a lean code snippet manager powered by GitHub Gist, built with Electron, React, and Redux. It provides a desktop application for managing and organizing code snippets with features like unlimited public/secret snippets, tagging, markdown/Jupyter notebook support, and GitHub Enterprise integration. ## Tech Stack - **Framework**: Electron (desktop app) - **Frontend**: React + Redux (with Redux Thunk for async actions, Redux Form for forms) - **Build System**: Webpack + Babel (ES6 transpilation) - **Styling**: Sass/SCSS - **Code Editor**: CodeMirror (via react-codemirror) - **Linting**: ESLint with Standard config - **Dependencies**: Uses yarn package manager ## Key Commands ### Development ```bash # Install dependencies yarn install # Development build and run yarn build && yarn start # Watch mode for development yarn webpack-watch # Production build yarn webpack-prod ``` ### Building & Distribution ```bash # Create installer for current platform yarn dist # Platform-specific builds yarn dist -m # macOS yarn dist -w # Windows yarn dist -l # Linux (requires Docker for snap) yarn dist -wml # All platforms ``` ### Code Quality ```bash # Lint code yarn lint # Check for outdated dependencies yarn check-outdated # Pre-version checks (runs lint + test + outdated check) yarn preversion ``` ### Testing The `yarn test` command runs webpack in development mode (essentially a build verification). There are no formal unit tests configured - the project relies on build-time checks and manual testing. ## Architecture ### Application Structure - `/app` - Main React application source - `/containers` - React container components (connected to Redux) - `/reducers` - Redux reducers for state management - `/actions` - Redux action creators - `/utilities` - Shared utilities (GitHub API, parser, search, etc.) - `/configs` - Configuration files including GitHub OAuth credentials - `/main.js` - Electron main process entry point - `/bundle` - Webpack build output directory ### Key Components - **GitHub API Integration**: `/app/utilities/githubApi/` - handles Gist CRUD operations - **Theme Management**: `/app/utilities/themeManager/` - light/dark theme switching - **Code Rendering**: Uses CodeMirror for syntax highlighting and editing - **Search**: `/app/utilities/search/` - snippet search functionality - **Configuration**: Uses nconf for config management, stored in `~/.leptonrc` ### GitHub OAuth Setup The app requires GitHub OAuth credentials in `/configs/account.js`: ```js module.exports = { client_id: '', client_secret: '' } ``` Register your application at https://github.com/settings/applications/new ## Development Notes - **Electron Version**: Uses Electron 13.x with @electron/remote for main-renderer communication - **ES6 Support**: Babel transpiles ES6+ to support older Electron versions - **Hot Reloading**: Use `yarn webpack-watch` for auto-rebuilding during development - **Styling**: Uses Sass with component-level SCSS files - **State Management**: Redux store handles application state, actions use Redux Thunk for async operations - **Shortcuts**: Customizable keyboard shortcuts defined in config, registered via electron-localshortcut ## Configuration The app uses a hierarchical configuration system: 1. Default config in `/configs/defaultConfig.js` 2. User config in `~/.leptonrc` (JSON format) 3. Environment variables and command line args Key configuration areas include theme, shortcuts, proxy settings, editor preferences, and GitHub Enterprise support. ## Important Guidelines When working with this codebase: - **NEVER modify `node_modules/`** - this directory contains installed dependencies and should not be edited - **DO NOT commit `yarn.lock`** unless specifically updating dependencies - this file locks dependency versions - **DO NOT change LICENSE files** unless told - Focus code changes on the `/app` directory, `/configs`, `main.js`, and configuration files - Avoid searching or reading files in `node_modules/`, `/bundle`, `/build`, `/dist` directories unless absolutely necessary ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017-present CosmoX Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![All Contributors](https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square)](#contributors-) ![](./docs/img/new_logo.png) [![MIT Licensed](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) [![lepton](https://snapcraft.io/lepton/badge.svg)](https://snapcraft.io/lepton) **Lepton** is a lean code snippet manager powered by GitHub Gist. [Check out the latest release.](https://github.com/hackjutsu/Lepton/releases) ## Features - Unlimited public/secret snippets - Unlimited tags - Language groups - Markdown/JupyterNotebook - [GitHub Enterprise](https://github.com/hackjutsu/Lepton/wiki/FAQ#enable-github-enterprise) - GitHub token - Immersive mode - [Customizable](https://github.com/hackjutsu/Lepton/wiki/Configuration) - Light/Dark theme - macOS/Win/Linux - Dashboard - [Search](https://github.com/hackjutsu/Lepton/wiki/FAQ#search) - [Proxy](https://github.com/hackjutsu/Lepton/wiki/FAQ#proxy) - Free ![Screenshot](./docs/img/portfolio/stay_organized.png) | [Light Theme](https://github.com/hackjutsu/Lepton#customization) | [Dark Theme](https://github.com/hackjutsu/Lepton#customization) | | :-------------:| :-----:| |![Screenshot](./docs/img/portfolio/lepton-light.png)|![Screenshot](./docs/img/portfolio/lepton-dark.png)| | Organize | Markdown | Jupyter Notebook | | :-------------:| :-----:| :-----: | | ![Screenshot](./docs/img/portfolio/stay_organized.png) | ![Screenshot](./docs/img/portfolio/markdown.png) | ![Screenshot](./docs/img/portfolio/jupyterNotebook.png) | | Search (*⇧ + Space*) | Immersive Mode *(⌘/Ctrl + i)* | Dashboard *(⌘/Ctrl + d)* | | :-------------:| :-----:| :-----: | | ![Screenshot](./docs/img/portfolio/search_bar.png) | ![Screenshot](./docs/img/portfolio/immersive.png) | ![Screenshot](./docs/img/portfolio/dashboard.png) ## Shortcuts | Function | Shortcut | Note | | :------------: |:-------------: |:-----:| | New Snippet | `Cmd/Ctrl + N` | Create a snippet | | Edit Snippet | `Cmd/Ctrl + E` | Edit a snippet | | Delete Snippet | `Cmd/Ctrl + Del` | Delete selected snippet | | Submit | `Cmd/Ctrl + S` | Submit the changes from the editor | | Cancel | `Cmd/Ctrl + ESC` | Exit the editor without saving | | Sync | `Cmd/Ctrl + R` | Sync with remote Gist server | | Immersive Mode | `Cmd/Ctrl + I` | Toggle the [Immersive mode](https://github.com/hackjutsu/Lepton/blob/master/docs/img/portfolio/immersive.png) | | Dashboard | `Cmd/Ctrl + D` | Toggle the [dashboard](https://github.com/hackjutsu/Lepton/blob/master/docs/img/portfolio/dashboard.png) | | About Page | `Cmd/Ctrl + ,` | Toggle the [About page](https://github.com/hackjutsu/Lepton/blob/dev/docs/img/portfolio/about.png) | | Search | `Shift + Space`| Toggle the [search bar](https://github.com/hackjutsu/Lepton/blob/master/docs/img/portfolio/search_bar.png) | ## Customization Lepton's can be customized by `/.leptonrc`! You can find its exact path in the About page by `Command/Ctrl + ,`. Create the file if it does not exist. - Theme (light/dark) - Snippet - Editor - Logger - Proxy - Shortcuts - Enterprise - Notifications Check out the [configuration docs](https://github.com/hackjutsu/Lepton/wiki/Configuration) to explore different customization options. ## Tech Stack ![Based on](./docs/img/erb-logo.png) 1. Framework: [Electron](http://electron.atom.io/) 2. Bundler: [Webpack](http://webpack.github.io/docs/), [Babel](https://babeljs.io), [electron-builder](https://github.com/electron-userland/electron-builder) 3. Language: [ES6](https://babeljs.io/docs/learn-es2015/), [Sass](http://sass-lang.com/) 4. Library: [React](https://facebook.github.io/react/), [Redux](https://github.com/reactjs/redux), [Redux Thunk](https://github.com/gaearon/redux-thunk), [Redux Form](http://redux-form.com/) 5. Lint: [ESLint](http://eslint.org/) Now you can learn about Lepton project's code structure in [DeepWiki](https://deepwiki.com/hackjutsu/Lepton)! ## Installation - macOS/Windows/Linux: Download [the released packages](https://github.com/hackjutsu/Lepton/releases) - macOS: Install via Homebrew ```bash brew install --cask lepton ``` - Linux: Install via [Snap Store](https://snapcraft.io/lepton) ```bash snap install lepton ``` ![Based on](./docs/img/lepton-ubuntu-tweet2.png) ## Development ### Install dependencies ```bash $ git clone https://github.com/hackjutsu/Lepton.git $ cd Lepton && yarn install ``` ```bash # inspect stale dependencies $ yarn check-outdated ``` ### Client ID/Secret [Register your application](https://github.com/settings/applications/new), and put your client id and client secret in `./configs/account.js`. ```js module.exports = { client_id: , client_secret: } ``` ### Run ```bash $ yarn build && yarn start ``` ## Build Installer App >Read [electron-builder docs](https://github.com/electron-userland/electron-builder#readme) and check out the [code signing wiki](https://github.com/electron-userland/electron-builder#code-signing) before building the installer app. Build apps for macOS. ```bash $ yarn dist -m ``` Build apps for Windows. ```bash $ yarn dist -w ``` Build apps for Linux. >Need a running [Docker](https://www.docker.com/) daemon to build a `snap` package. ```bash $ yarn dist -l ``` Build apps for macOS, Windows and Linux. ```bash $ yarn dist -wml ``` Build apps for the current OS with the current arch. ```bash $ yarn dist ``` ## FAQ [--> Wiki FAQ](https://github.com/hackjutsu/Lepton/wiki/FAQ) ## Contributors
CosmoX
CosmoX

💻 🎨 ⚠️ 🚧 📦 🤔
Jiaye Wu
Jiaye Wu

💻 🚧 🤔
Danila
Danila

💻 🎨 🚧
Meilin Zhan
Meilin Zhan

💻 🤔 🚧
lcgforever
lcgforever

💻
Yuer Lee
Yuer Lee

📖 📦
Su,Yen-Yun
Su,Yen-Yun

📖
Chen Tong
Chen Tong

💻 🤔 🚧
Jason Jiang
Jason Jiang

💻
Alan Pope
Alan Pope

📦
Tony Xu
Tony Xu

📦
Tegan Churchill
Tegan Churchill

💻
Angie Wang
Angie Wang

🎨
Batuhan Bayrakci
Batuhan Bayrakci

💻
Samuel Meuli
Samuel Meuli

💻
Alexandre Amado de Castro
Alexandre Amado de Castro

💻
Abner Soares Alves Junior
Abner Soares Alves Junior

💻
Sean
Sean

💻
Ole
Ole

💻
Gabriel Nicolas Avellaneda
Gabriel Nicolas Avellaneda

💻 📖
Dennis Ideler
Dennis Ideler

💻 🤔 📖
Anthony Attard
Anthony Attard

💻
ArLE
ArLE

💻
Pol Maresma
Pol Maresma

💻
PM Extra
PM Extra

💻
Zava
Zava

💻
Jason R Alexander
Jason R Alexander

💻
Sebastian Hojas
Sebastian Hojas

📖
董雨航
董雨航

💻
sxyazi
sxyazi

📦
Brian Zalewski
Brian Zalewski

📦
## License MIT © [hackjutsu](https://github.com/hackjutsu) ================================================ FILE: app/actions/index.js ================================================ import { getGitHubApi, GET_SINGLE_GIST } from '../utilities/githubApi' import { notifyFailure } from '../utilities/notifier' import { remote } from 'electron' const logger = remote.getGlobal('logger') export const UPDATE_USER_SESSION = 'UPDATE_USER_SESSION' export const LOGOUT_USER_SESSION = 'LOGOUT_USER_SESSION' export const UPDATE_ACCESS_TOKEN = 'UPDATE_ACCESS_TOKEN' export const REMOVE_ACCESS_TOKEN = 'REMOVE_ACCESS_TOKEN' export const UPDATE_GISTS = 'UPDATE_GISTS' export const UPDATE_SYNC_TIME = 'UPDATE_SYNC_TIME' export const UPDATE_SINGLE_GIST = 'UPDATE_SINGLE_GIST' export const UPDATE_GIST_TAGS = 'UPDATE_GIST_TAGS' export const UPDATE_PINNED_TAGS = 'UPDATE_PINNED_TAGS' export const SELECT_GIST_TAG = 'SELECT_GIST_TAG' export const SELECT_GIST = 'SELECT_GIST' export const UPDATE_AUTHWINDOW_STATUS = 'UPDATE_AUTHWINDOW_STATUS' export const UPDATE_GIST_SYNC_STATUS = 'UPDATE_GIST_SYNC_STATUS' export const UPDATE_SEARCHWINDOW_STATUS = 'UPDATE_SEARCHWINDOW_STATUS' export const UPDATE_SCROLL_REQUEST_STATUS = 'UPDATE_SCROLL_REQUEST_STATUS' export const UPDATE_UPDATEAVAILABLEBAR_STATUS = 'UPDATE_UPDATEAVAILABLEBAR_STATUS' export const UPDATE_NEW_VERSION_INFO = 'UPDATE_NEW_VERSION_INFO' export const UPDATE_IMMERSIVE_MODE_STATUS = 'UPDATE_IMMERSIVE_MODE_STATUS' export const UPDATE_LOGOUT_MODAL_STATUS = 'UPDATE_LOGOUT_MODAL_STATUS' export const UPDATE_GIST_RAW_MODAL = 'UPDATE_GIST_RAW_MODAL' export const UPDATE_GIST_EDIT_MODAL = 'UPDATE_GIST_EDIT_MODAL' export const UPDATE_GIST_NEW_MODAL = 'UPDATE_GIST_NEW_MODAL' export const UPDATE_GIST_DELETE_MODAL_STATUS = 'UPDATE_GIST_DELETE_MODAL_STATUS' export const UPDATE_PINNED_TAGS_MODAL_STATUS = 'UPDATE_PINNED_TAGS_MODAL_STATUS' export const UPDATE_FILE_EXPAND_STATUS = 'UPDATE_FILE_EXPAND_STATUS' export const UPDATE_DASHBOARD_MODAL_STATUS = 'UPDATE_DASHBOARD_MODAL_STATUS' export const UPDATE_ABOUT_MODAL_STATUS = 'UPDATE_ABOUT_MODAL_STATUS' export function updateDashboardModalStatus (status) { return { type: UPDATE_DASHBOARD_MODAL_STATUS, payload: status } } export function updateAboutModalStatus (status) { return { type: UPDATE_ABOUT_MODAL_STATUS, payload: status } } export function updateFileExpandStatus (status) { return { type: UPDATE_FILE_EXPAND_STATUS, payload: status } } export function updatePinnedTagsModalStatus (status) { return { type: UPDATE_PINNED_TAGS_MODAL_STATUS, payload: status } } export function updateGistDeleteModeStatus (status) { return { type: UPDATE_GIST_DELETE_MODAL_STATUS, payload: status } } export function updateGistEditModeStatus (status) { return { type: UPDATE_GIST_EDIT_MODAL, payload: status } } export function updateGistNewModeStatus (status) { return { type: UPDATE_GIST_NEW_MODAL, payload: status } } export function updateGistRawModal (modalInfo) { return { type: UPDATE_GIST_RAW_MODAL, payload: modalInfo } } export function updateImmersiveModeStatus (status) { return { type: UPDATE_IMMERSIVE_MODE_STATUS, payload: status } } export function updateNewVersionInfo (status) { return { type: UPDATE_NEW_VERSION_INFO, payload: status } } export function updateUpdateAvailableBarStatus (status) { return { type: UPDATE_UPDATEAVAILABLEBAR_STATUS, payload: status } } export function updateGistSyncStatus (status) { return { type: UPDATE_GIST_SYNC_STATUS, payload: status } } export function updateAuthWindowStatus (status) { return { type: UPDATE_AUTHWINDOW_STATUS, payload: status } } export function updateSearchWindowStatus (status) { return { type: UPDATE_SEARCHWINDOW_STATUS, payload: status } } export function updatescrollRequestStatus (status) { return { type: UPDATE_SCROLL_REQUEST_STATUS, payload: status } } export function updateAccessToken (token) { return { type: UPDATE_ACCESS_TOKEN, payload: token } } export function removeAccessToken () { return { type: REMOVE_ACCESS_TOKEN, payload: null } } export function updateUserSession (session) { return { type: UPDATE_USER_SESSION, payload: session } } export function logoutUserSession () { return { type: LOGOUT_USER_SESSION, payload: null } } export function updateGists (gists) { return { type: UPDATE_GISTS, payload: gists } } export function updateSyncTime (time) { return { type: UPDATE_SYNC_TIME, payload: time } } export function updateSingleGist (gist) { return { type: UPDATE_SINGLE_GIST, payload: gist } } export function updateGistTags (tags) { return { type: UPDATE_GIST_TAGS, payload: tags } } export function updatePinnedTags (tags) { return { type: UPDATE_PINNED_TAGS, payload: tags } } export function selectGistTag (tag) { return { type: SELECT_GIST_TAG, payload: tag } } export function selectGist (id) { return { type: SELECT_GIST, payload: id } } export function updateLogoutModalStatus (status) { return { type: UPDATE_LOGOUT_MODAL_STATUS, payload: status } } export function fetchSingleGist (oldGist, id) { return (dispatch, getState) => { const state = getState() return getGitHubApi(GET_SINGLE_GIST)(state.accessToken, id) .then((details) => { const newGist = Object.assign(oldGist, { details: details }) const newGistWithId = {} newGistWithId[id] = newGist dispatch(updateSingleGist(newGistWithId)) }) .catch((err) => { logger.error('The request has failed: ' + err) notifyFailure('Sync failed', 'Please check your network condition. 01') }) } } ================================================ FILE: app/containers/aboutPage/index.js ================================================ import fs from 'fs' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { Modal, Image } from 'react-bootstrap' import { remote, shell } from 'electron' import { updateAboutModalStatus } from '../../actions' import defaultConfig from '../../../configs/defaultConfig' import appInfo from '../../../package.json' import LicenseInfo from '../../../license.json' import ContributorInfo from '../../../.all-contributorsrc' import logoDarkImage from './logo-dark.png' import logoLightImage from './logo-light.png' import React, { Component } from 'react' import './index.scss' const conf = remote.getGlobal('conf') const logFilePath = remote.getGlobal('logFilePath') const configFilePath = remote.getGlobal('configFilePath') class AboutPage extends Component { openFileInEditor (filePath) { if (!fs.existsSync(filePath)) { fs.closeSync(fs.writeFileSync(filePath, JSON.stringify(defaultConfig, null, 2))) } shell.openPath(filePath) } renderAboutSection () { // Get the contributor list const contributorList = [] const contributors = ContributorInfo.contributors || [] // just in case the format changed contributors.forEach(item => { const contributorProfileLink = `https://github.com/${item.login}` contributorList.push( ) }) // Get the license list const licenseList = [] // Add Evil icons license as an exception licenseList.push(
Octodex Images
octodex.github.com
,
Evil icons@1.9.0
License: MIT
) Object.keys(LicenseInfo).forEach(item => { if (item.startsWith(appInfo.name)) { return } licenseList.push(
{ item }
License: { LicenseInfo[item].licenses }
) }) const logoImage = conf.get('theme') === 'dark' ? logoDarkImage : logoLightImage return (
{ appInfo.name + ' v' + appInfo.version }
GitHub Feedback License
Configurations
{ configFilePath }
Logs
{ logFilePath }
Contributors
{ contributorList }
Acknowledgement
{ licenseList }
) } renderSettingModalBody () { return (
{ this.renderAboutSection() }
) } handleCloseButtonClicked () { const { updateAboutModalStatus } = this.props updateAboutModalStatus('OFF') } render () { return ( About { this.renderSettingModalBody() } ) } } function mapStateToProps (state) { return { aboutModalStatus: state.aboutModalStatus } } function mapDispatchToProps (dispatch) { return bindActionCreators({ updateAboutModalStatus: updateAboutModalStatus }, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(AboutPage) ================================================ FILE: app/containers/aboutPage/index.scss ================================================ .about-modal { .font-style-base { font-family: Consolas, Menlo, Monaco, "Courier New", monospace; font-size: 13px; color: var(--text-secondary); } .section-base { height: 80vh; } .general-section { @extend .section-base; } .about-section { @extend .section-base; display: flex; flex-direction: column; color: var(--text-secondary); } .logo-section { @extend .font-style-base; font-size: 12px; text-align: center; margin-bottom: 10px; } .logo { width: 100px; margin: 5px; } .contributor-section { overflow: auto; min-height: 150px; border-radius: 5px; padding: 0 20px; border: solid 1px var(--border-color); color: var(--text-secondary); } .license-section { overflow: auto; flex: auto; margin-bottom: 20px; border-radius: 5px; padding: 0 20px; border: solid 1px var(--border-color); color: var(--text-secondary); } .one-line-section { overflow: auto; min-height: 30px; border-radius: 5px; padding: 4px 10px 0px 10px; white-space: nowrap; border: solid 1px var(--border-color); color: var(--text-secondary); } .license-item { margin: 10px 0px; } .license-project { @extend .font-style-base; font-weight: bold; } .license-type { @extend .font-style-base; font-size: 11px; } .setting-title { @extend .font-style-base; font-size: 14px; font-weight: bold; margin-top: 10px; margin-left: 5px; margin-bottom: 5px; } .setting-title-clickable { @extend .setting-title; } .setting-title-clickable:hover { cursor: pointer; } .modal-sm { width: 375px; } .logo-sub { margin: 0 5px; } .contributor { @extend .font-style-base; margin: 5px; } hr { margin-bottom: 5px; } /* Settings only affecting the about modal */ .modal { -webkit-app-region: drag; background: rgba(0, 0, 0, 0.5); } } ================================================ FILE: app/containers/appContainer/index.js ================================================ import { Alert } from 'react-bootstrap' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { remote, shell } from 'electron' import { updateUpdateAvailableBarStatus } from '../../actions/index' import AboutPage from '../aboutPage' import Dashboard from '../dashboard' import electronLocalStorage from 'electron-json-storage-sync' import LoginPage from '../loginPage' import NavigationPanel from '../navigationPanel' import NavigationPanelDetails from '../navigationPanelDetails' import React, { Component } from 'react' import SearchPage from '../searchPage' import SnippetPanel from '../snippetPanel' import SplitPane from 'react-split-pane' import ThemeManager from '../../utilities/themeManager' import './index.scss' import './scrollbar.scss' const conf = remote.getGlobal('conf') const themeManager = new ThemeManager() themeManager.setTheme(conf.get('theme')) class AppContainer extends Component { renderAboutPage () { const { updateAboutModalStatus } = this.props return ( ) } renderDashboard () { const { updateDashboardStatus } = this.props return ( ) } renderSearchPage () { const { searchWindowStatus, searchIndex } = this.props return (
{ searchWindowStatus === 'OFF' ? null : }
) } dismissUpdateAlert () { const { updateUpdateAvailableBarStatus } = this.props updateUpdateAvailableBarStatus('OFF') } handleDownloadClicked () { const { newVersionInfo } = this.props shell.openExternal(newVersionInfo.url) this.dismissUpdateAlert() } handleReleaseNotesClicked () { shell.openExternal('https://github.com/hackjutsu/Lepton/releases') this.dismissUpdateAlert() } handleSkipClicked () { const { newVersionInfo } = this.props electronLocalStorage.set('skipped-version', newVersionInfo.version) this.dismissUpdateAlert() } renderUpdateAlert () { const { updateAvailableBarStatus, newVersionInfo } = this.props return (
{ updateAvailableBarStatus === 'ON' ? { `New version ${newVersionInfo.version} is available! ` } #skip { newVersionInfo.url ? #release : #download } { newVersionInfo.url ? #download : null } : null }
) } renderActiveNormalSection () { const { updateLocalStorage, updateActiveGistAfterClicked, reSyncUserGists, localPref, searchIndex } = this.props return (
{ this.renderAboutPage() } { this.renderDashboard() } { this.renderSearchPage() } { this.renderUpdateAlert() }
) } renderActiveImmersiveSection () { const { searchIndex, reSyncUserGists } = this.props return ( ) } renderActiveSection () { const { immersiveMode } = this.props return (
{ immersiveMode === 'ON' ? this.renderActiveImmersiveSection() : this.renderActiveNormalSection() }
) } renderInactiveSection () { const { loggedInUserInfo, launchAuthWindow } = this.props return ( ) } render () { const { userSession } = this.props return (
{ userSession.activeStatus === 'ACTIVE' ? this.renderActiveSection() : this.renderInactiveSection() }
) } } function mapStateToProps (state) { return { userSession: state.userSession, searchWindowStatus: state.searchWindowStatus, aboutModalStatus: state.aboutModalStatus, dashboardModalStatus: state.dashboardModalStatus, newVersionInfo: state.newVersionInfo, updateAvailableBarStatus: state.updateAvailableBarStatus, immersiveMode: state.immersiveMode } } function mapDispatchToProps (dispatch) { return bindActionCreators({ updateUpdateAvailableBarStatus: updateUpdateAvailableBarStatus }, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(AppContainer) ================================================ FILE: app/containers/appContainer/index.scss ================================================ .app-container { margin: 0px; max-height: 100vh; } .app-container { .font-style-base { font-family: Consolas, Menlo, Monaco, "Courier New", monospace; font-size: 13px; } .bar-button { width: 100px; } .customized-button { @extend .font-style-base; float: right; margin: 5px 5px 0px 10px; } .customized-button:hover { cursor: pointer; } .SplitPane { position: relative !important; } .alert { margin-bottom: 0px; } } /*For the 'x' close button at top-right corner of the modal*/ div > div.modal-header > button > span:nth-child(1) { color: var(--modal-close-button); } div > div.modal-header > button { color: var(--modal-close-button); } /**/ /*For the resizer handle*/ .Resizer { background: #000; opacity: .2; z-index: 1; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; -moz-background-clip: padding; -webkit-background-clip: padding; background-clip: padding-box; } .Resizer:hover { -webkit-transition: all 1s ease; transition: all 1s ease; } .Resizer.horizontal { height: 11px; margin: -2px 0; border-top: 2px solid rgba(0, 0, 0, 0); border-bottom: 2px solid rgba(0, 0, 0, 0); cursor: row-resize; width: 100%; } .Resizer.horizontal:hover { border-top: 2px solid rgba(0, 0, 0, 0.0); border-bottom: 2px solid rgba(0, 0, 0, 0.0); } .Resizer.vertical { width: 2px; margin: 0 -2px; border-left: 2px solid rgba(0, 0, 0, 0); border-right: 2px solid rgba(0, 0, 0, 0); cursor: col-resize; } .Resizer.vertical:hover { border-left: 2px solid rgba(0, 0, 0, 0.0); border-right: 2px solid rgba(0, 0, 0, 0.0); } .Resizer.disabled { cursor: not-allowed; } .Resizer.disabled:hover { border-color: transparent; } .btn-default { color: var(--text-primary); background-color: transparent; border-color: #ccc; } ================================================ FILE: app/containers/appContainer/scrollbar.scss ================================================ *:not(.modal):not(:hover)::-webkit-scrollbar { display: none; } ::-webkit-scrollbar { width: 8px; height: 5px; } ::-webkit-scrollbar-thumb { border-radius: 2px; background: #f5f5f5; -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5); } ================================================ FILE: app/containers/codeArea/index.js ================================================ import HighlightJS from 'highlight.js' import hljsDefineSolidity from 'highlightjs-solidity' import hljsDefineGraphQL from 'highlightjs-graphql' import Markdown from '../../utilities/markdown' import nb from '../../utilities/jupyterNotebook' import React, { Component } from 'react' import '../../utilities/vendor/prism/prism.scss' import './jupyterNotebook.scss' import './markdown.scss' const remote = require('@electron/remote') const logger = remote.getGlobal('logger') const conf = remote.getGlobal('conf') // resolve syntax highlight style based on app theme if (conf.get('theme') === 'dark') { require('../../utilities/vendor/highlightJS/styles/atom-one-dark.css') } else { require('../../utilities/vendor/highlightJS/styles/github-gist.css') } hljsDefineSolidity(HighlightJS) // register solidity to hightlight.js hljsDefineGraphQL(HighlightJS) // register graphql to hightlight.js export default class CodeArea extends Component { createJupyterNotebookCodeBlock (content, language, kTabLength) { try { const notebook = nb.parse(JSON.parse(content)) const notebookHtml = notebook.render().outerHTML return `
${notebookHtml}
` } catch (err) { logger.error(`Failed to render Jupyter Notebook content with err ${err}`) return this.createHighlightedCodeBlock(content, language, kTabLength) } } createMarkdownCodeBlock (content) { return `
${Markdown.render(content)}
` } createHighlightedCodeBlock (content, language, kTabLength) { let lineNumber = 0 const highlightedContent = HighlightJS.highlightAuto(content, [language]).value /* Highlight.js wraps comment blocks inside . However, when the multi-line comment block is broken down into different table rows, only the first row, which is appended by the tag, is highlighted. The following code fixes it by appending to each line of the comment block. */ const commentPattern = /(.|\n)*?\*\/\s*<\/span>/g const adaptedHighlightedContent = highlightedContent.replace(commentPattern, data => { return data.replace(/\r?\n/g, () => { // Chromium is smart enough to add the closing return "\n" }) }) const contentTable = adaptedHighlightedContent.split(/\r?\n/).map(lineContent => { return ` ${lineContent === '' ? '\n' : lineContent} ` }).join('') return `
${contentTable}
` } // Find the best language for code highlighting by best effort. adaptedLanguage (filename, lang) { let language = lang || 'Other' // Adjust the language based on file extensions. const filenameExtension = filename.split('.').pop() switch (filenameExtension) { case 'leptonrc': language = 'json' break case 'zshrc': language = 'bash' break case 'sql': language = 'sql' break case 'solidity': case 'sol': language = 'solidity' break default: // intentionally left blank } // Adapt the language name for Highlight.js. For example, 'C#' should be // expressed as 'cs' to be recognized by Highlight.js. switch (language) { case 'Shell': return 'bash' case 'C#': return 'cs' case 'Objective-C': return 'objectivec' case 'Objective-C++': return 'objectivec' case 'Visual Basic': return 'vbscript' case 'Batchfile': return 'bat' default: } return language } renderCodeArea (filename, content, lang, kTabLength) { const language = this.adaptedLanguage(filename, lang) let htmlContent = '' switch (language) { case 'Jupyter Notebook': htmlContent = this.createJupyterNotebookCodeBlock(content, language, kTabLength) break case 'Markdown': htmlContent = this.createMarkdownCodeBlock(content) break default: htmlContent = this.createHighlightedCodeBlock(content, language, kTabLength) } return (
) } render () { const { filename, content, language, kTabLength } = this.props return this.renderCodeArea(filename, content, language, kTabLength) } } ================================================ FILE: app/containers/codeArea/jupyterNotebook.scss ================================================ .font-style-base { font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, Ubuntu, Droid Sans, sans-serif; font-size: 14px; } .jupyterNotebook-section { h1 { @extend .font-style-base; font-size: 30px !important; font-weight: 600; margin-top: 20px !important; margin-bottom: 10px !important; } h2 { @extend .font-style-base; font-size: 25px !important; margin-top: 20px !important; margin-bottom: 10px !important; } .nb-notebook { @extend .font-style-base; line-height: 1.5; margin-top: 20px; margin-bottom: 20px; margin-left: 100px; margin-right: 50px; } .nb-stdout, .nb-stderr { @extend .font-style-base; white-space: pre-wrap; margin: 1em 0; padding: 0.1em 0.5em; } .nb-stderr { @extend .font-style-base; background-color: #FAA; } .nb-cell + .nb-cell { margin-top: 0.5em; } .nb-output table { border: 1px solid #000; border-collapse: collapse; } .nb-output th { font-weight: bold; } .nb-output th, .nb-output td { @extend .font-style-base; border: 1px solid #000; padding: 0.25em; text-align: left; vertical-align: middle; border-collapse: collapse; } .nb-cell { @extend .font-style-base; position: relative; } .nb-raw-cell { @extend .font-style-base; white-space: pre-wrap; background-color: #f5f2f0; padding: 1em; margin: .5em 0; } .nb-cell.nb-code-cell .nb-input { border-style: solid; border-radius: 5px; border-color: lightgray; border-width: 1px; padding-top: 5px; padding-bottom: 5px; padding-left: 10px; padding-right: 10px; font-size: 12px; } .nb-output { @extend .font-style-base; min-height: 1em; width: 100%; overflow-x: scroll; } .nb-output img { max-width: 100%; } .nb-input:before { position: absolute; font-family: monospace; color: #999; left: -7.5em; width: 7em; text-align: right; padding: 10px; font-size: 12px; } .nb-output:before { position: absolute; font-family: monospace; color: #999; left: -7.5em; width: 7em; text-align: right; padding: 10px; padding-left: 8px; font-size: 12px; } .nb-input:before { content: "In [" attr(data-prompt-number) "]:"; } .nb-output:before { content: "Out [" attr(data-prompt-number) "]:"; } .nb-input, .nb-output{ font-size: 13px; } // Fix pandas dataframe formatting div[style="max-height:1000px;max-width:1500px;overflow:auto;"] { max-height: none !important; } code { padding: 2px 4px; font-size: 90%; color: var(--text-primary); background-color: transparent; } } ================================================ FILE: app/containers/codeArea/markdown.scss ================================================ // GitHub style markdown css .markdown-section { body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Segoe UI Symbol'; font-size: 14px; line-height: 1.6 !important; padding-top: 10px; padding-bottom: 10px; background-color: var(--bg-primary); padding: 30px; } body > *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } a { color: #4183C4; text-decoration: none; } a.absent { color: #cc0000; } a.anchor { display: block; padding-left: 30px; margin-left: -30px; cursor: pointer; position: absolute; top: 0; left: 0; bottom: 0; } h1, h2, h3, h4, h5, h6 { margin: 20px 0 10px; padding: 0; font-weight: bold; -webkit-font-smoothing: antialiased; cursor: text; position: relative; } h2:first-child, h1:first-child, h1:first-child + h2, h3:first-child, h4:first-child, h5:first-child, h6:first-child { margin-top: 0; padding-top: 0; } h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor { text-decoration: none; } h1 tt, h1 code { font-size: inherit; } h2 tt, h2 code { font-size: inherit; } h3 tt, h3 code { font-size: inherit; } h4 tt, h4 code { font-size: inherit; } h5 tt, h5 code { font-size: inherit; } h6 tt, h6 code { font-size: inherit; } h1 { font-size: 28px; } h2 { font-size: 24px; border-bottom: 1px solid #cccccc; } h3 { font-size: 18px; } h4 { font-size: 16px; } h5 { font-size: 14px; } h6 { color: #777777; font-size: 14px; } p, blockquote, ul, ol, dl, li, table, pre { margin: 15px 0; line-height: 1.5; } li { margin: 0; } hr { background: transparent repeat-x 0 0; border: 0 none; color: #cccccc; height: 4px; padding: 0; } body > h2:first-child { margin-top: 0; padding-top: 0; } body > h1:first-child { margin-top: 0; padding-top: 0; } body > h1:first-child + h2 { margin-top: 0; padding-top: 0; } body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child { margin-top: 0; padding-top: 0; } a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { margin-top: 0; padding-top: 0; } h1 p, h2 p, h3 p, h4 p, h5 p, h6 p { margin-top: 0; } li p.first { display: inline-block; } ul, ol { padding-left: 30px; } ul :first-child, ol :first-child { margin-top: 0; } ul :last-child, ol :last-child { margin: 0; } dl { padding: 0; } dl dt { font-size: 14px; font-weight: bold; font-style: italic; padding: 0; margin: 15px 0 5px; } dl dt:first-child { padding: 0; } dl dt > :first-child { margin-top: 0; } dl dt > :last-child { margin-bottom: 0; } dl dd { margin: 0 0 15px; padding: 0 15px; } dl dd > :first-child { margin-top: 0; } dl dd > :last-child { margin-bottom: 0; } blockquote { border-left: 4px solid #dddddd; padding: 0 15px; color: #777777; font-size: 15px; } blockquote > :first-child { margin-top: 0; } blockquote > :last-child { margin-bottom: 0; } table { padding: 0; } table tr { border-top: 1px solid #cccccc; margin: 0; padding: 0; } table tr th { font-weight: bold; border: 1px solid #cccccc; text-align: left; margin: 0; padding: 6px 13px; } table tr td { border: 1px solid #cccccc; text-align: left; margin: 0; padding: 6px 13px; } table tr th :first-child, table tr td :first-child { margin-top: 0; } table tr th :last-child, table tr td :last-child { margin-bottom: 0; } img { max-width: 100%; } span.frame { display: block; overflow: hidden; } span.frame > span { border: 1px solid #dddddd; display: block; float: left; overflow: hidden; margin: 13px 0 0; padding: 7px; width: auto; } span.frame span img { display: block; float: left; } span.frame span span { clear: both; display: block; padding: 5px 0 0; } span.align-center { display: block; overflow: hidden; clear: both; } span.align-center > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: center; } span.align-center span img { margin: 0 auto; text-align: center; } span.align-right { display: block; overflow: hidden; clear: both; } span.align-right > span { display: block; overflow: hidden; margin: 13px 0 0; text-align: right; } span.align-right span img { margin: 0; text-align: right; } span.float-left { display: block; margin-right: 13px; overflow: hidden; float: left; } span.float-left span { margin: 13px 0 0; } span.float-right { display: block; margin-left: 13px; overflow: hidden; float: right; } span.float-right > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: right; } code, tt { margin: 0 2px; white-space: nowrap; border:1px solid var(--border-color); background-color: var(--md-inline-code-background); border-radius: 3px; color: var(--text-primary); } pre code { margin: 0; padding: 0; white-space: pre-wrap; border: none; background: transparent; } .highlight pre { background-color: #f8f7fa; border: 1px solid var(--border-color); font-size: 13px; line-height: 19px; overflow: auto; padding: 12px 12px; border-radius: 3px; } pre { padding: 9.5px !important; margin: 0 0 10px !important; font-size: 13px !important; line-height: 1.42857143; color: #333; word-break: break-all; word-wrap: break-word; // background-color: #f5f5f5 !important; border: 1px solid var(--border-color) !important; border-radius: 3px !important; } pre code, pre tt { background-color: transparent; border: none; } img { max-width: 80%; display: block; margin: 0 auto; } } ================================================ FILE: app/containers/dashboard/index.js ================================================ import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { Modal, Image } from 'react-bootstrap' import { Radar } from 'react-chartjs' import { updateDashboardModalStatus } from '../../actions' import React, { Component } from 'react' import robotocatImage from '../../utilities/octodex/robotocat.png' import './index.scss' class Dashboard extends Component { renderDashboardSection () { const { gistTags } = this.props const langTags = Object.keys(gistTags) .filter(key => key.startsWith('lang@') && key !== 'lang@All') .sort((t1, t2) => gistTags[t2].length - gistTags[t1].length) const maxNum = 5 const rawLabels = langTags.slice(0, langTags.length > maxNum ? maxNum : langTags.length) const data = rawLabels.map(lang => gistTags[lang].length) const labels = rawLabels.map(rawLabel => rawLabel.substr('@lang'.length)) if (data.length > 2) return this.buildRadarChart(data, labels) return (
Not enough data. Try creating gists of more than two languages. Happy Coding!
) } // https://github.com/chartjs/Chart.js/blob/v1.1.1/docs/03-Radar-Chart.md buildRadarChart (data, labels) { const GREY = '#C2C4D1' const chartData = { labels: labels, datasets: [ { label: 'My Language Stats', fillColor: 'rgba(81,192,191,0.2)', strokeColor: 'rgba(81,192,191,1)', pointColor: 'rgba(81,192,191,1)', pointStrokeColor: GREY, pointHighlightFill: GREY, pointHighlightStroke: 'rgba(81,192,191,1)', data: data } ] } const chartOptions = { pointLabelFontSize: 12, } return (
{ this.renderCompliments(labels[0]) }
) } renderCompliments (lang) { if (lang === 'Other') return 'Hmm... Looks like you are learning a mysterious language.' return (
Well done! You are on the road to
{ `${lang} Master!` }
) } renderSettingModalBody () { return (
{ this.renderDashboardSection() }
) } handleCloseButtonClicked () { const { updateDashboardModalStatus } = this.props updateDashboardModalStatus('OFF') } render () { return ( Dashboard { this.renderSettingModalBody() } ) } } function mapStateToProps (state) { return { dashboardModalStatus: state.dashboardModalStatus, gistTags: state.gistTags } } function mapDispatchToProps (dispatch) { return bindActionCreators({ updateDashboardModalStatus: updateDashboardModalStatus }, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(Dashboard) ================================================ FILE: app/containers/dashboard/index.scss ================================================ .dashboard-modal { .font-style-base { font-family: Consolas, Menlo, Monaco, "Courier New", monospace; font-size: 14px; color: rgb(100,100,100); } .section-base { height: 400px; } .dashboard-section { @extend .section-base; display: flex; flex-direction: column; } .compliment { @extend .font-style-base; text-align: center; margin-top: 20px; } .compliment-word { margin: 2px; } .octocat { width: 300px; margin: 5px; } .modal-sm { width: 375px; } /* Settings only affecting the about modal */ .modal { -webkit-app-region: drag; background: rgba(0, 0, 0, 0.5); } } ================================================ FILE: app/containers/gistEditor/index.js ================================================ import React, { Component } from 'react' import CodeMirror from 'react-codemirror' import 'codemirror/mode/meta' // When Webpack imports these AMD modules for modes, the first one will be set as // the default mode for CodeMirror, which then analyzes the content accordingly // under the hood for syntax highlight. However, some mode will report errors if // the content doesn't comply with its syntax, like dylan which doesn't allow the // word 'constructor' (though this could be a bug in CodeMirror). Therefore the // first mode matters. We intentionally select markdown as the first(default) mode // because of its flexibility in syntax. import 'codemirror/mode/markdown/markdown' import 'codemirror/mode/dylan/dylan' import 'codemirror/mode/textile/textile' import 'codemirror/mode/pegjs/pegjs' import 'codemirror/mode/factor/factor' import 'codemirror/mode/asterisk/asterisk' import 'codemirror/mode/crystal/crystal' import 'codemirror/mode/sql/sql' import 'codemirror/mode/pug/pug' import 'codemirror/mode/forth/forth' import 'codemirror/mode/lua/lua' import 'codemirror/mode/mumps/mumps' import 'codemirror/mode/elm/elm' import 'codemirror/mode/tornado/tornado' import 'codemirror/mode/d/d' import 'codemirror/mode/rpm/rpm' import 'codemirror/mode/gfm/gfm' import 'codemirror/mode/swift/swift' import 'codemirror/mode/solr/solr' import 'codemirror/mode/stex/stex' import 'codemirror/mode/haskell/haskell' import 'codemirror/mode/velocity/velocity' import 'codemirror/mode/properties/properties' import 'codemirror/mode/ruby/ruby' import 'codemirror/mode/htmlembedded/htmlembedded' import 'codemirror/mode/smarty/smarty' import 'codemirror/mode/tiddlywiki/tiddlywiki' import 'codemirror/mode/go/go' import 'codemirror/mode/ttcn/ttcn' import 'codemirror/mode/slim/slim' import 'codemirror/mode/tiki/tiki' import 'codemirror/mode/sieve/sieve' import 'codemirror/mode/troff/troff' import 'codemirror/mode/z80/z80' import 'codemirror/mode/eiffel/eiffel' import 'codemirror/mode/yaml/yaml' import 'codemirror/mode/powershell/powershell' import 'codemirror/mode/vhdl/vhdl' import 'codemirror/mode/turtle/turtle' import 'codemirror/mode/ebnf/ebnf' import 'codemirror/mode/livescript/livescript' import 'codemirror/mode/haml/haml' import 'codemirror/mode/jinja2/jinja2' import 'codemirror/mode/php/php' import 'codemirror/mode/mathematica/mathematica' import 'codemirror/mode/htmlmixed/htmlmixed' import 'codemirror/mode/gherkin/gherkin' import 'codemirror/mode/q/q' import 'codemirror/mode/ecl/ecl' import 'codemirror/mode/perl/perl' import 'codemirror/mode/vue/vue' import 'codemirror/mode/ntriples/ntriples' import 'codemirror/mode/cmake/cmake' import 'codemirror/mode/handlebars/handlebars' import 'codemirror/mode/modelica/modelica' import 'codemirror/mode/dockerfile/dockerfile' import 'codemirror/mode/yaml-frontmatter/yaml-frontmatter' import 'codemirror/mode/groovy/groovy' import 'codemirror/mode/oz/oz' import 'codemirror/mode/twig/twig' import 'codemirror/mode/pascal/pascal' import 'codemirror/mode/diff/diff' import 'codemirror/mode/idl/idl' import 'codemirror/mode/rst/rst' import 'codemirror/mode/smalltalk/smalltalk' import 'codemirror/mode/nsis/nsis' import 'codemirror/mode/mbox/mbox' import 'codemirror/mode/spreadsheet/spreadsheet' import 'codemirror/mode/haskell-literate/haskell-literate' import 'codemirror/mode/dart/dart' import 'codemirror/mode/octave/octave' import 'codemirror/mode/mirc/mirc' import 'codemirror/mode/haxe/haxe' import 'codemirror/mode/mllike/mllike' import 'codemirror/mode/shell/shell' import 'codemirror/mode/asn.1/asn.1' import 'codemirror/mode/clike/clike' import 'codemirror/mode/css/css' import 'codemirror/mode/pig/pig' import 'codemirror/mode/xquery/xquery' import 'codemirror/mode/asciiarmor/asciiarmor' import 'codemirror/mode/erlang/erlang' import 'codemirror/mode/scheme/scheme' import 'codemirror/mode/python/python' import 'codemirror/mode/coffeescript/coffeescript' import 'codemirror/mode/clojure/clojure' import 'codemirror/mode/fcl/fcl' import 'codemirror/mode/puppet/puppet' import 'codemirror/mode/brainfuck/brainfuck' import 'codemirror/mode/http/http' import 'codemirror/mode/dtd/dtd' import 'codemirror/mode/r/r' import 'codemirror/mode/verilog/verilog' import 'codemirror/mode/xml/xml' import 'codemirror/mode/javascript/javascript' import 'codemirror/mode/webidl/webidl' import 'codemirror/mode/vbscript/vbscript' import 'codemirror/mode/toml/toml' import 'codemirror/mode/vb/vb' import 'codemirror/mode/rust/rust' import 'codemirror/mode/cypher/cypher' import 'codemirror/mode/jsx/jsx' import 'codemirror/mode/django/django' import 'codemirror/mode/ttcn-cfg/ttcn-cfg' import 'codemirror/mode/cobol/cobol' import 'codemirror/mode/commonlisp/commonlisp' import 'codemirror/mode/soy/soy' import 'codemirror/mode/stylus/stylus' import 'codemirror/mode/sparql/sparql' import 'codemirror/mode/nginx/nginx' import 'codemirror/mode/sas/sas' import 'codemirror/mode/mscgen/mscgen' import 'codemirror/mode/gas/gas' import 'codemirror/mode/fortran/fortran' import 'codemirror/mode/apl/apl' import 'codemirror/mode/julia/julia' import 'codemirror/mode/tcl/tcl' import 'codemirror/mode/protobuf/protobuf' import 'codemirror/mode/yacas/yacas' import 'codemirror/mode/sass/sass' import 'codemirror/addon/edit/matchbrackets' import 'codemirror/addon/edit/matchtags' import 'codemirror/addon/dialog/dialog' import 'codemirror/addon/mode/loadmode' import 'codemirror/addon/search/search' import 'codemirror/addon/search/match-highlighter' import 'codemirror/addon/search/searchcursor' import 'codemirror/addon/fold/comment-fold' import 'codemirror/addon/fold/xml-fold' import 'codemirror/addon/fold/foldgutter' import 'codemirror/addon/fold/indent-fold' import 'codemirror/addon/fold/brace-fold' import 'codemirror/addon/fold/foldcode' import 'codemirror/addon/fold/markdown-fold' import 'codemirror/addon/display/placeholder' import './index.scss' import { remote } from 'electron' const conf = remote.getGlobal('conf') const highlightTheme = conf.get('theme') === 'dark' ? 'one-dark' : 'github' const defaultOptions = Object.assign({}, { theme: highlightTheme, lineNumbers: true, matchBrackets: true, matchTags: true, lineWrapping: true, viewportMargin: Infinity, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] }, conf.get('editor')) class GistEditor extends Component { componentDidMount () { const { filename } = this.props this.editor = this.refs.editor this.CodeMirror = this.editor.getCodeMirrorInstance() this.CodeMirror.modeURL = '../../../node_modules/codemirror/mode/%N/%N.js' this.setMode(filename) } componentDidUpdate (prevProps, prevState) { const { filename } = this.props if (prevProps.filename !== filename) { this.setMode(filename) } } setMode (filename) { const modeInfo = filename ? this.CodeMirror.findModeByFileName(filename) : this.CodeMirror.findModeByName('Plain Text') modeInfo && this.editor.getCodeMirror().setOption('mode', modeInfo.mime) } render () { const { value, placeholder } = this.props const options = Object.assign({}, defaultOptions, { placeholder }) return ( this.props.onChange(value) } /> ) } } export default GistEditor ================================================ FILE: app/containers/gistEditor/index.scss ================================================ .font-style-base { font-family: Consolas, Menlo, Monaco, "Courier New", monospace; font-size: 13px; } .CodeMirror { @extend .font-style-base; height: auto; } .CodeMirror-gutters { border-right: 1px solid var(--border-color); background-color: var(--bg-secondary); } // To overwrite the gutter color under one-dark theme .cm-s-one-dark .CodeMirror-gutter, .cm-s-one-dark .CodeMirror-gutters { border: none; background-color: var(--bg-secondary); } .CodeMirror-scroll { min-height: 400px; background: var(--bg-primary); } .CodeMirror-empty { color: #969896 } .cm-s-github .cm-comment { color: #969896; } .cm-s-github .cm-string { color: #183691 } .cm-s-github .cm-string-2 { color: #0086b3 } .cm-s-github .cm-tag { color: #63a35c } .cm-s-github .cm-keyword, .cm-s-github .cm-operator { color: #a71d5d } .cm-s-github .cm-property { color: #333 } .cm-s-github .cm-atom { color: #0086b3 } .cm-s-github .cm-error, .cm-s-github .cm-invalidchar { font-style: italic; font-weight: 600; color: #bd2c00; text-decoration: underline } .cm-s-github .cm-variable-2, .cm-s-github .cm-number { color: #0086b3 } @import '../../../node_modules/codemirror/lib/codemirror.css'; @import '../../../node_modules/codemirror/mode/tiddlywiki/tiddlywiki.css'; @import '../../../node_modules/codemirror/mode/tiki/tiki.css'; @import '../../../node_modules/codemirror/addon/dialog/dialog.css'; @import '../../../node_modules/codemirror/addon/fold/foldgutter.css'; @import '../../../node_modules/codemirror-one-dark-theme/one-dark.css'; ================================================ FILE: app/containers/gistEditorForm/index.js ================================================ import { connect } from 'react-redux' import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form' import { remote, ipcRenderer } from 'electron' import { OverlayTrigger, Tooltip, Button, ListGroup, ListGroupItem, Panel } from 'react-bootstrap' import GistEditor from '../gistEditor' import React, { Component } from 'react' import validFilename from 'valid-filename' import tipsIcon from './ei-question.svg' import './index.scss' export const NEW_GIST = 'NEW_GIST' export const UPDATE_GIST = 'UPDATE_GIST' const conf = remote.getGlobal('conf') const logger = remote.getGlobal('logger') const descriptionTips = '[title] description #tag1 #tag2' const tooltip = ( { descriptionTips } ) class GistEditorFormImpl extends Component { componentWillMount () { const { change, initialData } = this.props // Initialize the form initialData.private && change('private', initialData.private) initialData.description && change('description', initialData.description) initialData.gists && change('gistFiles', initialData.gists) } componentDidMount () { ipcRenderer.on('submit-gist', () => { this.shortcutSubmit() }) } componentWillUnmount () { ipcRenderer.removeAllListeners('submit-gist') } shortcutSubmit () { // https://github.com/erikras/redux-form/issues/1304 const submitter = this.props.handleSubmit(this.props.onSubmit) submitter() // submits } render () { const { handleSubmit, handleCancel, submitting, formStyle, filenameList } = this.props return (

) } } const valideNotEmptyContent = value => value ? null : 'required' const validateFilename = value => { // empty filename is not allowed if (!value) { return 'required' } // validate filename according to the .leptonrc configs if (!conf.get('editor').validateFilename) { logger.info('[Filename Validation] According to the config, filename validation has been skipped') } else if (!validFilename(value)) { return 'invalid filename' } } const renderTitleInputField = ({ input, placeholder, type, meta: { touched, error, warning } }) => (
{ touched && ((error && { error }) || (warning && { warning })) }
) const renderDescriptionField = ({ input, type, meta: { touched, error, warning } }) => (
{ touched && ((error && { error }) || (warning && { warning })) } ) const renderContentField = ({ input, type, meta: { touched, error, warning }, filename }) => (
{ touched && ((error && {error}) || (warning && {warning})) }
) function renderGistFileHeader (member, fields, index) { return ( ) } const renderGistFiles = ({ fields, formStyle, filenameList }) => ( { fields.map((member, index) => { renderGistFileHeader(member, fields, index) } ) } ) const selector = formValueSelector('gistEditorForm') const GistEditorForm = connect( state => { const gistFiles = selector(state, 'gistFiles') const filenameList = gistFiles && gistFiles.map(({ filename }) => filename) return { filenameList } } )(GistEditorFormImpl) export default reduxForm({ form: 'gistEditorForm' })(GistEditorForm) ================================================ FILE: app/containers/gistEditorForm/index.scss ================================================ .gist-editor-form { .font-style-base { font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, Ubuntu, Droid Sans, sans-serif; font-size: 14px; color: var(--text-primary); } .font-style-blue { @extend .font-style-base; color: #4078C0 !important; } .gist-editor-control-button { margin: 5px; float: right; } .gist-editor-new-gist-button { margin: 5px 0px; } .gist-editor-section { margin: 5px 5px; } .gist-editor-name { display: flex; align-items: center; padding-right: 15px; } .gist-editor-section .panel-body { padding: 0px !important; } .gist-editor-section .panel-heading { padding: 2px 5px !important; color: inherit; border-color: var(--border-color); background-color: var(--bg-secondary); } .gist-editor-input-area { @extend .font-style-base; flex: 1 1 95%; padding: 5px 10px; border: solid 1px; border-color: var(--border-color); border-radius: 3px; margin-right: 10px; background: var(--bg-primary); } .gist-editor-filename-area { @extend .font-style-base; width: 80%; margin: 5px 20px 5px 0px; padding: 5px 10px; border: solid 1px; border-color: var(--border-color); border-radius: 3px; background: var(--bg-primary); } .panel { border-color: var(--border-color); border-radius: 0; } .gist-editor-content-area { @extend .font-style-base; width: 100%; padding: 5px; min-height: 300px; border: 0px; resize: none; } .gist-editor-gist-file { border: 0px; padding: 10px 0px !important; background: var(--text-bg-primary); } .control-button-group { height: 45px; } .gist-editor-customized-tag { @extend .font-style-blue; margin-left: 5px; } .gist-editor-customized-tag-hidden { @extend .gist-editor-customized-tag; visibility: hidden; } .gist-editor-privacy-checkbox { @extend .font-style-base; display: inline; float: right; } .title-input-field { display: inline; } .error-msg { @extend .font-style-base; font-size: 12px; font-weight: bold; color: #B0580E; } .tips { @extend .font-style-base; font-size: 13px; font-style: italic; flex: 0 0 auto; position: relative; & > span { vertical-align: middle; } } .tips-icon { height: 24px; width: 24px; display: inline-block; vertical-align: middle; fill: currentColor; } hr { margin-top: 20px; margin-bottom: 20px; border: 1; border-top: 1px solid var(--border-color); } } ================================================ FILE: app/containers/loginPage/index.js ================================================ import { Alert, Button, Image, Modal, ProgressBar } from 'react-bootstrap' import Avatar from 'boring-avatars' import { connect } from 'react-redux' import { remote, ipcRenderer } from 'electron' import React, { Component } from 'react' import dojocatImage from '../../utilities/octodex/dojocat.jpg' import privateinvestocatImage from '../../utilities/octodex/privateinvestocat.jpg' import saritocatImage from '../../utilities/octodex/saritocat.png' import './index.scss' const conf = remote.getGlobal('conf') const logger = remote.getGlobal('logger') const LoginModeEnum = { CREDENTIALS: 1, TOKEN: 2 } class LoginPage extends Component { constructor (props) { super(props) this.state = { inputTokenValue: '', loginMode: LoginModeEnum.CREDENTIALS } } componentWillMount () { const { loggedInUserInfo } = this.props logger.debug('-----> Inside LoginPage componentWillMount with loggedInUserInfo' + JSON.stringify(loggedInUserInfo)) logger.debug('-----> Registering listener for auto-login signal') ipcRenderer.on('auto-login', () => { logger.debug('-----> Received "auto-login" signal with loggedInUserInfo ' + JSON.stringify(loggedInUserInfo)) loggedInUserInfo && loggedInUserInfo.token && this.handleContinueButtonClicked(loggedInUserInfo.token) }) logger.debug('-----> sending login-page-ready signal') ipcRenderer.send('login-page-ready') } componentWillUnmount () { logger.debug('-----> Removing listener for auto-login signal') ipcRenderer.removeAllListeners('auto-login') } handleLoginClicked () { if (this.props.authWindowStatus === 'OFF') { this.props.launchAuthWindow() } } handleContinueButtonClicked (token) { this.handleTokenLoginButtonClicked(token) } handleTokenLoginButtonClicked (token) { if (token && this.props.authWindowStatus === 'OFF') { this.props.launchAuthWindow(token) } } handleLoginModeSwitched () { if (this.state.loginMode === LoginModeEnum.CREDENTIALS) { this.setState({ loginMode: LoginModeEnum.TOKEN }) } else { this.setState({ loginMode: LoginModeEnum.CREDENTIALS }) } } renderControlSection () { const { authWindowStatus, loggedInUserInfo, userSessionStatus } = this.props const { loginMode } = this.state const loggedInUserName = loggedInUserInfo ? loggedInUserInfo.profile : null const welcomeMessage = 'Lepton is FREE. Like us on GitHub! ⭐' if (userSessionStatus === 'IN_PROGRESS') { return ( ) } if (conf.get('enterprise:enable')) { const token = conf.get('enterprise:token') return (
{ token ? : this.renderTokenLoginSection(false, userSessionStatus)}
) } if (userSessionStatus === 'EXPIRED' || userSessionStatus === 'INACTIVE' || loggedInUserName === null || loggedInUserName === 'null') { return (
{ loginMode === LoginModeEnum.CREDENTIALS ? this.renderCredentialLoginSection(authWindowStatus, userSessionStatus) : this.renderTokenLoginSection(true, userSessionStatus) }
) } return null } updateInputValue (evt) { this.setState({ inputTokenValue: evt.target.value }) } renderCredentialLoginSection (authWindowStatus, userSessionStatus) { return (
{ userSessionStatus === 'EXPIRED' ? Token invalid : null }
) } renderTokenLoginSection (showLoginSwitch, userSessionStatus) { return (
{ userSessionStatus === 'EXPIRED' ? Token invalid : null } { showLoginSwitch ? : null}
) } renderLoginModalBody () { return (
{ this.renderAvatar() } { this.renderControlSection() }
) } renderAvatar () { const { loginMode } = this.state if (conf.get('avatar:type') === 'boring') { return } else { let profileImage = dojocatImage if (conf.get('enterprise:enable')) { profileImage = conf.get('enterprise:avatarUrl') ? conf.get('enterprise:avatarUrl') : privateinvestocatImage } else if (loginMode === LoginModeEnum.TOKEN) { profileImage = saritocatImage } return } } render () { return (
Login { this.renderLoginModalBody() }
) } } function mapStateToProps (state) { return { authWindowStatus: state.authWindowStatus, userSessionStatus: state.userSession.activeStatus } } export default connect(mapStateToProps)(LoginPage) ================================================ FILE: app/containers/loginPage/index.scss ================================================ .login-modal { .modal-button-base { margin-top: 5px; margin-bottom: 5px; width: 100%; } .modal { -webkit-user-select: none; -webkit-app-region: drag; background: #808080; } .modal-content { margin-top: 100px; min-height: 400px; } .profile-image-modal { width: 200px; height: 200px; margin: 5px; } .button-group-modal { margin: 20px 20px 0px 20px; } .modal-button { @extend .modal-button-base; } .modal-button:focus { outline: none; } .modal-button-disabled { @extend .modal-button-base; cursor: not-allowed; } .modal-button-disabled:hover { cursor: not-allowed; } .login-page-text-link { font-size: 12px; font-style: italic; color: gray; margin-top: 10px; margin-bottom: 10px; text-decoration: underline; } .login-alert { margin-bottom: 5px !important; } } ================================================ FILE: app/containers/navigationPanel/index.js ================================================ import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { Modal, Button } from 'react-bootstrap' import { parseLangName as Resolved } from '../../utilities/parser' import { remote } from 'electron' import React, { Component } from 'react' import UserPanel from '../userPanel' import { fetchSingleGist, selectGist, selectGistTag, updatePinnedTags, updatePinnedTagsModalStatus } from '../../actions' import plusIcon from './plus.svg' import './index.scss' const conf = remote.getGlobal('conf') const logger = remote.getGlobal('logger') class NavigationPanel extends Component { constructor (props) { super(props) const { localPref, userSession } = this.props const userName = userSession.profile.login let activeSection = -1 if (localPref && localPref.get(userName)) { const cachedActiveSession = localPref.get(userName).activeSection if (cachedActiveSession !== undefined) { activeSection = cachedActiveSession } } logger.debug(`-----> The tag activeSection is ${activeSection}`) this.state = { tmpPinnedTags: new Set(), activeSection, } } handleClicked (key) { const { selectGistTag, updateActiveGistAfterClicked, gists, gistTags } = this.props selectGistTag(key) updateActiveGistAfterClicked(gists, gistTags, key) } renderPinnedTags () { const { pinnedTags, gistTags, activeGistTag } = this.props const pinnedTagList = [] pinnedTags.forEach(tag => { if (gistTags[tag]) { pinnedTagList.push( ) } }) return pinnedTagList } // renderPinnedTags renderLangTags () { const { gistTags, activeGistTag } = this.props const langTagList = [] Object.keys(gistTags) .filter(tag => { return tag.startsWith('lang@') }) .sort() .forEach(prefixedLang => { langTagList.push( ) }) return langTagList } // renderLangTags() renderCustomTags () { const { gistTags, activeGistTag } = this.props const customTagList = [] Object.keys(gistTags) .filter(tag => { return !tag.startsWith('lang@') }) .sort() .forEach(prefixedLang => { customTagList.push( ) }) return customTagList } handleConfigurePinnedTagClicked () { const { updatePinnedTagsModalStatus, pinnedTags } = this.props this.setState({ tmpPinnedTags: new Set(pinnedTags) }) updatePinnedTagsModalStatus('ON') } handleSectionClick (index) { const { activeSection } = this.state const { localPref, userSession } = this.props const nextActiveSection = activeSection === index ? -1 : index this.setState({ activeSection: nextActiveSection }) // Saving the activeSection to local preference const userName = userSession.profile.login logger.debug(`-----> Saving new tag activeSection ${nextActiveSection}`) localPref.set(userName, Object.assign({}, localPref.get(userName), { activeSection: nextActiveSection })) } renderTagSection () { const { userSession } = this.props const { activeSection } = this.state let gitHubHost = 'github.com' if (conf.get('enterprise:enable')) { gitHubHost = conf.get('enterprise:host') } return (
Languages
{ this.renderLangTags() }
Pinned
{ this.renderPinnedTags() }
Tags
{ this.renderCustomTags() }
) } handleTagInPinnedTagsModalClicked (tag) { const { tmpPinnedTags } = this.state tmpPinnedTags.has(tag) ? tmpPinnedTags.delete(tag) : tmpPinnedTags.add(tag) this.setState({ tmpPinnedTags: tmpPinnedTags }) } closePinnedTagsModal () { this.props.updatePinnedTagsModalStatus('OFF') this.setState({ tmpPinnedTags: new Set() }) } renderAllTagsForPin () { const { gistTags } = this.props const { tmpPinnedTags } = this.state const langTags = [] const customTags = [] Object.keys(gistTags).sort().forEach(item => { item.startsWith('lang@') ? langTags.push(item) : customTags.push(item) }) const orderedGistTags = [...customTags, ...langTags] const tagsForPinRows = [] let i = 1 let row = [] orderedGistTags.forEach(tag => { row.push( #{ tag.startsWith('lang@') ? Resolved(tag) : tag } ) if (i++ % 5 === 0) { tagsForPinRows.push({ row }) row = [] } }) row && tagsForPinRows.push({ row }) return ( { tagsForPinRows }
) } handlePinnedTagSaved () { const { tmpPinnedTags } = this.state const { updatePinnedTags, userSession, localPref } = this.props const pinnedTags = Array.from(tmpPinnedTags) logger.info('[Dispatch] updatePinnedTags') updatePinnedTags(pinnedTags) this.closePinnedTagsModal() // Saving the pinnedTags to local preference const userName = userSession.profile.login localPref.set(userName, Object.assign({}, localPref.get(userName), { pinnedTags })) } renderPinnedTagsModal () { const { pinnedTagsModalStatus } = this.props return ( Shortcuts { this.renderAllTagsForPin() } ) } render () { const { searchIndex, updateLocalStorage, getLoggedInUserInfo, reSyncUserGists, launchAuthWindow } = this.props return (

{ this.renderTagSection() } { this.renderPinnedTagsModal() }
) } } function mapStateToProps (state) { return { gists: state.gists, gistTags: state.gistTags, pinnedTags: state.pinnedTags, userSession: state.userSession, activeGistTag: state.activeGistTag, pinnedTagsModalStatus: state.pinnedTagsModalStatus } } function mapDispatchToProps (dispatch) { return bindActionCreators({ selectGistTag: selectGistTag, selectGist: selectGist, fetchSingleGist: fetchSingleGist, updatePinnedTags: updatePinnedTags, updatePinnedTagsModalStatus: updatePinnedTagsModalStatus }, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(NavigationPanel) ================================================ FILE: app/containers/navigationPanel/index.scss ================================================ .menu-panel { padding: 4px 7px; float: left; width: 140px; height: 100vh; display: flex; flex-direction: column; border-right: 1px solid var(--border-color); color: var(--text-primary); background-color: var(--bg-secondary); } .menu-panel { hr { margin-top: 15px; margin-bottom: 15px; width: 100%; border-top: 1px solid var(--border-color); } .font-style-base { font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, Ubuntu, Droid Sans, sans-serif; font-size: 14px; } .gist-tag { @extend .font-style-base; color: var(--text-secondary-darken); cursor: pointer; transition: color .15s; &:hover { color: var(--text-primary) } } .configure-tag { padding: 3px; width: 10px; height: 14px; box-sizing: content-box; cursor: pointer; fill: var(--text-secondary); transition: fill .15s; svg { display: block; width: 100%; height: 100% } &:hover { fill: #413D45; } } .configure-tag-line { // margin-top: 15px; } .user-panel { flex: 0 0; } .gist-tag-section { margin-left: 0px; flex: 1 1 1px; display: flex; flex-direction: column; position: relative; } .tag-section-list { flex: 1 1 1px; display: flex; flex-direction: column; margin-top: 15px; // https://bugs.chromium.org/p/chromium/issues/detail?id=927066 // The item should not have a minimum height min-height: 0; } .tag-section-title { @extend .font-style-base; font-size: 13px; letter-spacing: .25px; text-transform: uppercase; font-weight: 500; color: inherit; margin-bottom: 6px; position: relative; transition: opacity .15s; &:hover { opacity: .65; } } .tag-section { flex: 1 0 33.333%; max-height: 33.333%; display: flex; flex-direction: column; padding: 15px 0; border-top: 1px solid var(--border-color); } .tag-section-active { flex: 1 1 auto; max-height: none; // https://bugs.chromium.org/p/chromium/issues/detail?id=927066 // The item should not have a minimum height min-height: 0; } .tag-section-hidden { flex: 0 0 auto; .tag-section-title, .pinned-tag-header { margin-bottom: 0; } .tag-section-content { height: 0; } } .tag-section-content { flex: 1 1 auto; overflow: auto; padding-left: 16px; } .active-gist-tag { @extend .font-style-base; color: var(--text-primary); font-weight: bold; cursor: pointer; } .pinned-tag-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 7px; .tag-section-title { margin-bottom: 0; } } } .pinned-tags-modal { .pin-tag-table { width: 100%; } .font-style-base { font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, Ubuntu, Droid Sans, sans-serif; font-size: 14px; } .gist-tag-not-pinned { @extend .font-style-base; margin: 5px; cursor: pointer; } .gist-tag-pinned { @extend .font-style-base; margin: 5px; color: rgb(3, 123, 183); font-weight: bold; cursor: pointer; } } ================================================ FILE: app/containers/navigationPanelDetails/index.js ================================================ import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { descriptionParser } from '../../utilities/parser' import { remote } from 'electron' import { selectGist, fetchSingleGist, updatescrollRequestStatus } from '../../actions' import React, { Component } from 'react' import './index.scss' const logger = remote.getGlobal('logger') const conf = remote.getGlobal('conf') class NavigationPanelDetails extends Component { componentDidUpdate () { const { updatescrollRequestStatus, scrollRequestStatus, activeGist } = this.props if (scrollRequestStatus === 'ON') { this.refs[activeGist].scrollIntoView(true) logger.info('[Dispatch] update scroll request to OFF') updatescrollRequestStatus('OFF') } } handleClicked (gistId) { const { gists, fetchSingleGist, selectGist } = this.props logger.info('A new gist is selected: ' + gistId) if (!gists[gistId].details) { logger.info('[Dispatch] fetchSingleGist ' + gistId) fetchSingleGist(gists[gistId], gistId) } logger.info('[Dispatch] selectGist ' + gistId) selectGist(gistId) } decideSnippetListItemClass (gistId) { if (gistId === this.props.activeGist) { if (this.props.gists[gistId].brief.public) { return 'active-snippet-thumnail-public' } else { return 'active-snippet-thumnail-private' } } return 'snippet-thumnail' } renderSnippetThumbnails () { const { gists, gistTags, activeGistTag } = this.props const snippetThumbnails = [] // When user has no gists, the default active language tag will be 'All' with // an empty array. if (!gistTags || !gistTags[activeGistTag] || gistTags[activeGistTag].length === 0) { return (
) } const rawGists = [] gistTags[activeGistTag].forEach((gistId) => { // During the synchronization, gists will be updated before the gistTags, // which introduces an interval where a gist exists in gistTags but not in // the gists. This guard makes sure we push the gist only when it is already // available in gists. if (gists[gistId]) { // Pick up the content inside the first [] as the snippet thumbnail's title. // For example, "[Apple is delicious] It's affordable and healthy." will // pick up "Apple is delicious" as the title. If no brackets are found, // it shows to the original description. It provides users the flexibility // to decide what to be shown in the thumbnail. const gist = gists[gistId] rawGists.push(gist) } }) const sortingKey = conf.get('snippet:sorting') const sortingReverse = conf.get('snippet:sortingReverse') if (sortingReverse) { rawGists.sort((g1, g2) => g2.brief[sortingKey].localeCompare(g1.brief[sortingKey])) } else { rawGists.sort((g1, g2) => g1.brief[sortingKey].localeCompare(g2.brief[sortingKey])) } rawGists.forEach((gist) => { const firstFilename = Object.keys(gist.brief.files)[0] // '' will be converted to false, so this statement can handle situations // for null, '' and undefined const rawDescription = gist.brief.description || firstFilename const { title, description } = descriptionParser(rawDescription) const thumbnailTitle = title.length > 0 ? title : description const gistId = gist.brief.id snippetThumbnails.push(
  • { thumbnailTitle }
  • ) }) return snippetThumbnails } // renderSnippetThumbnails() render () { return (
      { this.renderSnippetThumbnails() }
    ) } } function mapStateToProps (state) { return { gists: state.gists, gistTags: state.gistTags, activeGistTag: state.activeGistTag, activeGist: state.activeGist, scrollRequestStatus: state.scrollRequestStatus } } function mapDispatchToProps (dispatch) { return bindActionCreators({ selectGist: selectGist, fetchSingleGist: fetchSingleGist, updatescrollRequestStatus: updatescrollRequestStatus }, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(NavigationPanelDetails) ================================================ FILE: app/containers/navigationPanelDetails/index.scss ================================================ .panel-thumbnails-background { height: 100vh; position: relative; } .panel-thumbnails-scroll { float: left; width: 100%; overflow-y: overlay; height: 100%; visibility: visible; } // With .panel-thumbnails-scroll visible, this block is not needed // // .panel-thumbnails-content, // .panel-thumbnails-scroll:hover { // visibility: visible; // } .panel-thumbnails-scroll { .font-style-base { font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, Ubuntu, Droid Sans, sans-serif; font-size: 15px; } ul { padding-left: 0px; } ul:first-child { border-top-left-radius: 0px; border-top-right-radius: 0px; } ul:last-child { border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; // border-bottom: solid 1px var(--border-color); } .snippet-thumnail-list-item { padding: 0 !important; border-right-width: 0 !important; border-left-width: 0 !important; border-bottom: solid 1px var(--border-color); background: var(--bg-primary); cursor: pointer; } .snippet-thumnail-list-item:hover { background-color: var(--bg-secondary); } .snippet-thumnail { @extend .font-style-base; padding: 6px; overflow: hidden; text-overflow: ellipsis; } .active-snippet-thumnail-base { @extend .font-style-base; padding: 6px; font-weight: 500; background: var(--bg-secondary); } .active-snippet-thumnail-public { @extend .active-snippet-thumnail-base; // background: #DFF0D4; } .active-snippet-thumnail-private { @extend .active-snippet-thumnail-base; // background: #F2DEDE; } .snippet-thumnail-description { @extend .font-style-base; margin: 5px; overflow: hidden; text-overflow: ellipsis; } } ================================================ FILE: app/containers/searchPage/index.js ================================================ import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { Modal } from 'react-bootstrap' import { remote } from 'electron' import React, { Component } from 'react' import { addLangPrefix as Prefixed, descriptionParser, } from '../../utilities/parser' import { fetchSingleGist, selectGist, selectGistTag, updatescrollRequestStatus, updateSearchWindowStatus, } from '../../actions' import './index.scss' const logger = remote.getGlobal('logger') class SearchPage extends Component { constructor (props) { super(props) this.state = { inputValue: '', selectedIndex: 0, searchResults: [] } } componentWillMount () { const { searchIndex } = this.props searchIndex.initFuseSearch() } selectPreGist () { const { selectedIndex, searchResults } = this.state let newSelectedIndex = selectedIndex - 1 if (!searchResults || newSelectedIndex < 0) { newSelectedIndex = searchResults.length - 1 } this.setState({ selectedIndex: newSelectedIndex, }) this.refs[newSelectedIndex].scrollIntoView(false) } selectNextGist () { const { selectedIndex, searchResults } = this.state let newSelectedIndex = selectedIndex + 1 if (!searchResults || newSelectedIndex >= searchResults.length) { newSelectedIndex = 0 } this.setState({ selectedIndex: newSelectedIndex, }) this.refs[newSelectedIndex].scrollIntoView(false) } selectCurrentGist () { const { selectedIndex, searchResults } = this.state if (searchResults && searchResults.length > 0) { this.handleSnippetClicked(searchResults[selectedIndex].id) } } handleKeyDown (e) { const { updateSearchWindowStatus } = this.props if (e.keyCode === 40) { // Down e.preventDefault() this.selectNextGist() } else if (e.keyCode === 38) { // Up e.preventDefault() this.selectPreGist() } else if (e.keyCode === 13) { // Enter e.preventDefault() this.selectCurrentGist() } else if (e.keyCode === 27) { // Esc e.preventDefault() updateSearchWindowStatus('OFF') } } handleSnippetClicked (gistId) { const { gists, selectGistTag, selectGist, updateSearchWindowStatus, updatescrollRequestStatus, fetchSingleGist } = this.props if (!gists[gistId].details) { logger.info('[Dispatch] fetchSingleGist ' + gistId) fetchSingleGist(gists[gistId], gistId) } logger.info('[Dispatch] update scroll request to ON') updatescrollRequestStatus('ON') logger.info('[Dispatch] selectGist ' + gistId) selectGist(gistId) selectGistTag(Prefixed('All')) updateSearchWindowStatus('OFF') } updateInputValue (evt) { this.setState({ selectedIndex: 0, inputValue: evt.target.value }) } queryInputValue (evt) { const inputValue = evt.target.value const searchIndex = this.props.searchIndex const results = searchIndex.fuseSearch(inputValue) this.setState({ searchResults: results }) } renderSnippetDescription (rawDescription) { const { title, description } = descriptionParser(rawDescription) const htmlForDescriptionSection = [] if (title.length > 0) { htmlForDescriptionSection.push(
    { title }
    ) } htmlForDescriptionSection.push(
    { description }
    ) return (
    { htmlForDescriptionSection }
    ) } renderSearchResults () { const { searchResults, selectedIndex, inputValue } = this.state // FIXME: In some unknown circumstance, searchResults is undefined. So we put a // guard here. We should remove it once we better understand the mechanism // behind it. if (!inputValue || !searchResults) return null if (inputValue.length > 0 && searchResults.length === 0) { return (
    No result found...
    ) } const resultsJSXGroup = [] searchResults.forEach((gist, index) => { const highlightedDescription = gist.description let filenames = [] // FIXME: In some rare cases, gist.filename is undefined in runtime for some unknown reason. So // we place a guard here as a workaround. if (gist.filename) { filenames = gist.filename.split(',').filter(file => file.trim()).map(file => { return (
    { file }
    ) }) } resultsJSXGroup.push(
  • { this.renderSnippetDescription(highlightedDescription) }
    { filenames }
  • ) }) return resultsJSXGroup } renderSearchModalBody () { return (
      { this.renderSearchResults() }
    ) } render () { return (
    { this.renderSearchModalBody() }
    ) } } function mapStateToProps (state) { return { searchWindowStatus: state.authWindowStatus, userSessionStatus: state.userSession.activeStatus, gists: state.gists } } function mapDispatchToProps (dispatch) { return bindActionCreators({ selectGistTag: selectGistTag, selectGist: selectGist, fetchSingleGist: fetchSingleGist, updateSearchWindowStatus: updateSearchWindowStatus, updatescrollRequestStatus: updatescrollRequestStatus }, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(SearchPage) ================================================ FILE: app/containers/searchPage/index.scss ================================================ .search-modal { .font-style-base { font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, Ubuntu, Droid Sans, sans-serif; font-size: 13px; } .search-box { @extend .font-style-base; font-size: 16px; width: 100%; padding: 10px; border: 0; background: var(--bg-primary); } .search-box:focus { outline:none; } .result-group { max-height: 70vh; list-style-type: none; padding: 0px; overflow: auto; } .search-result-item { padding: 10px 10px; border-top: solid 1px var(--border-color); } .search-result-item:hover { background: rgba(0, 0, 0, 0.1); cursor: pointer; } .search-result-item-selected { @extend .search-result-item; background: rgba(0, 0, 0, 0.15); } .list-group { margin-bottom: 0px; } .list-group-item { border-left: 0; border-right: 0; } .list-group-item:first-child { border-top-left-radius: 0px; border-top-right-radius: 0px; } .list-group-item:last-child { border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } .snippet-description { @extend .font-style-base; } .not-found-msg { @extend .font-style-base; font-size: 15px; padding: 10px; color: var(--text-secondary); } .gist-tag-group { margin-top: 10px; } .gist-tag { display: inline; margin-right: 10px; font-size: 11px; color: #4078C0; } .gist-tag-selected { @extend .gist-tag; border:solid 2px red; } .modal-button-base { margin-top: 5px; margin-bottom: 5px; width: 100%; } .modal { -webkit-user-select: none; -webkit-app-region: drag; background: rgba(0, 0, 0, 0.5); } .modal-content { margin-top: 100px; } .tip { @extend .font-style-base; margin-top: -27px; margin-bottom: 10px; width: 100%; text-align: right; font-size: 10px; color: var(--text-secondary); font-style: italic; } .title-section { @extend .font-style-base; // font-size: 13px; font-weight: bold; margin-bottom: 10px; } .description-section { @extend .font-style-base; font-size: 12px; color: var(--text-secondary); } .custom-tags-section { @extend .font-style-base; color: var(--text-secondary); font-size: 10px; margin-left: 2px; margin-top: 3px; } } ================================================ FILE: app/containers/snippet/index.js ================================================ import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { default as GistEditorForm, UPDATE_GIST } from '../gistEditorForm' import { Panel, Modal, Button, ProgressBar, Collapse } from 'react-bootstrap' import { remote, clipboard, ipcRenderer } from 'electron' import Autolinker from 'autolinker' import CodeArea from '../codeArea' import HumanReadableTime from 'human-readable-time' import Moment from 'moment' import { notifySuccess, notifyFailure } from '../../utilities/notifier' import React, { Component } from 'react' import { addLangPrefix as Prefixed, descriptionParser, parseCustomTags, } from '../../utilities/parser' import { selectGistTag, updateFileExpandStatus, updateGistDeleteModeStatus, updateGistEditModeStatus, updateGistRawModal, updateGistTags, updateSingleGist, } from '../../actions' import { DELETE_SINGLE_GIST, EDIT_SINGLE_GIST, getGitHubApi, } from '../../utilities/githubApi' import './index.scss' import editIcon from './ei-edit.svg' import eyeIcon from './ei-eye.svg' import openInWebIcon from './ei-share.svg' import secretIcon from './lock.svg' import tagsIcon from './tags.svg' import trashIcon from './ei-trash.svg' const conf = remote.getGlobal('conf') const logger = remote.getGlobal('logger') const kIsExpanded = conf.get('snippet:expanded') const kTabLength = conf.get('editor:tabSize') class Snippet extends Component { componentDidMount () { ipcRenderer.on('edit-gist-renderer', () => { this.showGistEditorModal() }) ipcRenderer.on('exit-editor', () => { this.closeGistEditorModal() }) ipcRenderer.on('delete-gist', () => { this.showDeleteModal() }) } componentWillUnmount () { ipcRenderer.removeAllListeners('edit-gist-renderer') ipcRenderer.removeAllListeners('exit-editor') } showDeleteModal () { this.props.updateGistDeleteModeStatus('ON') } closeDeleteModal () { this.props.updateGistDeleteModeStatus('OFF') } handleDeleteClicked () { const { accessToken, activeGist } = this.props getGitHubApi(DELETE_SINGLE_GIST)( accessToken, activeGist) .catch(err => { logger.error('Failed to delete the gist ' + activeGist) logger.error(JSON.stringify(err)) notifyFailure('Deletion failed', 'Please check your network condition. 02') }) .then(data => { logger.info('The gist ' + activeGist + ' has been deleted.') notifySuccess('The gist has been deleted') // For performance purpose, we should perform an internal update, like what // we're doing for creating/edit gists. However, since delete is an infrequent // operation, we decide to just call the resync method and keep the logic // simple. this.props.reSyncUserGists() }) .finally(() => { this.closeDeleteModal() }) } renderDeleteModal () { return (
    Delete the gist?
    ) } showGistEditorModal () { const { gists, activeGist, updateGistEditModeStatus } = this.props if (gists && gists[activeGist] && gists[activeGist].details) { updateGistEditModeStatus('ON') } } closeGistEditorModal () { this.props.updateGistEditModeStatus('OFF') } handleGistEditorFormSubmit (data) { const { gists, activeGist, accessToken } = this.props const description = data.description.trim() const processedFiles = {} data.gistFiles.forEach((file) => { processedFiles[file.filename.trim()] = { content: file.content } }) const activeSnippet = gists[activeGist] for (const preFile in activeSnippet.details.files) { if (!processedFiles[preFile]) { processedFiles[preFile] = null } } // In the past, we close the dialog after getting response from the server // both for the “create” and “edit” actions. However, the state change for // the “edit” action is less intensive than “create”, for example, the active // language tag and the active gist are likely be the same as before. // Therefore, we decide the close the dialog without waiting for the server’s // response, which provides better user experience. this.closeGistEditorModal() return getGitHubApi(EDIT_SINGLE_GIST)( accessToken, activeGist, description, processedFiles) .catch((err) => { notifyFailure('Gist update failed') logger.error(JSON.stringify(err)) }) .then((response) => { this.updateGistsStoreWithUpdatedGist(response) }) } updateGistsStoreWithUpdatedGist (gistDetails) { const { gists, activeGist, gistTags, activeGistTag, updateSingleGist, updateGistTags, selectGistTag, searchIndex } = this.props const gistId = gistDetails.id const files = gistDetails.files const activeSnippet = gists[activeGist] const preLangs = activeSnippet.langs const preCustomTags = parseCustomTags(descriptionParser(activeSnippet.brief.description).customTags) // Adding files in an eidt could introduce some changes to the gistTags. // 1) if a gist has a new language, we should add the gist id to this // language tag, ie gistTags[language] 2) if the new language doesn't // exist, we should add the new language to gistTags. const newLangs = new Set() let filenameRecords = '' Object.keys(files).forEach(filename => { // leave a space in between to help tokenization filenameRecords += ', ' + filename const file = files[filename] const language = file.language || 'Other' newLangs.add(language) const prefixedLang = Prefixed(language) if (Object.prototype.hasOwnProperty.call(gistTags, prefixedLang)) { if (!gistTags[prefixedLang].includes(gistId)) { gistTags[prefixedLang].unshift(gistId) } } else { gistTags[prefixedLang] = [] gistTags[prefixedLang].unshift(gistId) } }) // Removing files in an eidt could introduce some changes to the gistTags. // 1) if a gist no long has a language, we should remove the gist id from // this language tag 2) if the updated language tag is empty, we should remove // this tag at all. preLangs.forEach(language => { if (!newLangs.has(language)) { const prefixedLang = Prefixed(language) gistTags[prefixedLang] = gistTags[prefixedLang].filter(value => { return value !== gistId }) if (gistTags[prefixedLang].length === 0) { delete gistTags[prefixedLang] } } }) // We update the custom tags with the similar reasons mentioned above const newCustomTags = parseCustomTags(descriptionParser(gistDetails.description).customTags) newCustomTags.forEach(tag => { if (Object.prototype.hasOwnProperty.call(gistTags, tag)) { if (!gistTags[tag].includes(gistId)) { gistTags[tag].unshift(gistId) } } else { gistTags[tag] = [] gistTags[tag].unshift(gistId) } }) preCustomTags.forEach(tag => { if (!newCustomTags.includes(tag)) { gistTags[tag] = gistTags[tag].filter(value => { return value !== gistId }) } if (gistTags[tag].length === 0) { delete gistTags[tag] } }) const updatedGist = {} updatedGist[gistId] = { langs: newLangs, brief: gistDetails, details: gistDetails, filename: filenameRecords } logger.info('[Dispatch] updateSingleGist') updateSingleGist(updatedGist) logger.info('[Dispatch] updateGistTags') updateGistTags(gistTags) // If the previous active language tag is no longer valid, for example, // user deletes all cpp files inside a gist when C++ tag is the ative tag, // or the gist array for the preivous active language tag is empty, we // choose to fall back to 'All'. if (!gistTags[activeGistTag] || !gistTags[activeGistTag].includes(gistId)) { logger.info('[Dispatch] selectGistTag') selectGistTag(Prefixed('All')) } // logger.info('[Dispatch] selectGist') // this.props.selectGist(gistId) // Update the search index let langSearchRecords = '' newLangs.forEach(lang => { langSearchRecords += ',' + lang }) searchIndex.updateFuseIndex({ id: gistId, description: gistDetails.description, language: langSearchRecords, filename: filenameRecords }) notifySuccess('Gist updated', HumanReadableTime(new Date())) } renderGistEditorModalBody (description, fileArray, isPrivate) { const initialData = Object.assign({ description: description, gists: fileArray, private: isPrivate }) return ( ) } renderGistEditorModal (description, fileArray, isPrivate) { return ( Edit { this.renderGistEditorModalBody(description, fileArray, isPrivate) } ) } closeRawModal () { this.props.updateGistRawModal({ status: 'OFF', file: null, content: null, link: null }) } handleCopyGistLinkClicked (snippet, file) { const link = snippet.details.html_url + '#file-' + file.filename.replace(/\./g, '-').toLowerCase() clipboard.writeText(link) notifySuccess('Copied', 'The link has been copied to the clipboard.') } handleCopyGistFileClicked (gist) { clipboard.writeText(gist.content) notifySuccess('Copied', 'The content has been copied to the clipboard.') } showRawModalModal (gist) { this.props.updateGistRawModal({ status: 'ON', file: gist.filename, content: gist.content, link: gist.raw_url }) } renderRawModal () { const { gistRawModal } = this.props return ( { gistRawModal.file } LINK "; support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; // #11217 - WebKit loses check when the name is after the checked attribute fragment.appendChild( div ); // Support: Windows Web Apps (WWA) // `name` and `type` must use .setAttribute for WWA (#14901) input = document.createElement( "input" ); input.setAttribute( "type", "radio" ); input.setAttribute( "checked", "checked" ); input.setAttribute( "name", "t" ); div.appendChild( input ); // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 // old WebKit doesn't clone checked state correctly in fragments support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; // Support: IE<9 // Cloned elements keep attachEvent handlers, we use addEventListener on IE9+ support.noCloneEvent = !!div.addEventListener; // Support: IE<9 // Since attributes and properties are the same in IE, // cleanData must set properties to undefined rather than use removeAttribute div[ jQuery.expando ] = 1; support.attributes = !div.getAttribute( jQuery.expando ); } )(); // We have to close these tags to support XHTML (#13200) var wrapMap = { option: [ 1, "" ], legend: [ 1, "
    ", "
    " ], area: [ 1, "", "" ], // Support: IE8 param: [ 1, "", "" ], thead: [ 1, "", "
    " ], tr: [ 2, "", "
    " ], col: [ 2, "", "
    " ], td: [ 3, "", "
    " ], // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, // unless wrapped in a div with non-breaking characters in front of it. _default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] }; // Support: IE8-IE9 wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; function getAll( context, tag ) { var elems, elem, i = 0, found = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName( tag || "*" ) : typeof context.querySelectorAll !== "undefined" ? context.querySelectorAll( tag || "*" ) : undefined; if ( !found ) { for ( found = [], elems = context.childNodes || context; ( elem = elems[ i ] ) != null; i++ ) { if ( !tag || jQuery.nodeName( elem, tag ) ) { found.push( elem ); } else { jQuery.merge( found, getAll( elem, tag ) ); } } } return tag === undefined || tag && jQuery.nodeName( context, tag ) ? jQuery.merge( [ context ], found ) : found; } // Mark scripts as having already been evaluated function setGlobalEval( elems, refElements ) { var elem, i = 0; for ( ; ( elem = elems[ i ] ) != null; i++ ) { jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[ i ], "globalEval" ) ); } } var rhtml = /<|&#?\w+;/, rtbody = / from table fragments if ( !support.tbody ) { // String was a , *may* have spurious elem = tag === "table" && !rtbody.test( elem ) ? tmp.firstChild : // String was a bare or wrap[ 1 ] === "
    " && !rtbody.test( elem ) ? tmp : 0; j = elem && elem.childNodes.length; while ( j-- ) { if ( jQuery.nodeName( ( tbody = elem.childNodes[ j ] ), "tbody" ) && !tbody.childNodes.length ) { elem.removeChild( tbody ); } } } jQuery.merge( nodes, tmp.childNodes ); // Fix #12392 for WebKit and IE > 9 tmp.textContent = ""; // Fix #12392 for oldIE while ( tmp.firstChild ) { tmp.removeChild( tmp.firstChild ); } // Remember the top-level container for proper cleanup tmp = safe.lastChild; } } } // Fix #11356: Clear elements from fragment if ( tmp ) { safe.removeChild( tmp ); } // Reset defaultChecked for any radios and checkboxes // about to be appended to the DOM in IE 6/7 (#8060) if ( !support.appendChecked ) { jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); } i = 0; while ( ( elem = nodes[ i++ ] ) ) { // Skip elements already in the context collection (trac-4087) if ( selection && jQuery.inArray( elem, selection ) > -1 ) { if ( ignored ) { ignored.push( elem ); } continue; } contains = jQuery.contains( elem.ownerDocument, elem ); // Append to fragment tmp = getAll( safe.appendChild( elem ), "script" ); // Preserve script evaluation history if ( contains ) { setGlobalEval( tmp ); } // Capture executables if ( scripts ) { j = 0; while ( ( elem = tmp[ j++ ] ) ) { if ( rscriptType.test( elem.type || "" ) ) { scripts.push( elem ); } } } } tmp = null; return safe; } ( function() { var i, eventName, div = document.createElement( "div" ); // Support: IE<9 (lack submit/change bubble), Firefox (lack focus(in | out) events) for ( i in { submit: true, change: true, focusin: true } ) { eventName = "on" + i; if ( !( support[ i ] = eventName in window ) ) { // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) div.setAttribute( eventName, "t" ); support[ i ] = div.attributes[ eventName ].expando === false; } } // Null elements to avoid leaks in IE. div = null; } )(); var rformElems = /^(?:input|select|textarea)$/i, rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, rtypenamespace = /^([^.]*)(?:\.(.+)|)/; function returnTrue() { return true; } function returnFalse() { return false; } // Support: IE9 // See #13393 for more info function safeActiveElement() { try { return document.activeElement; } catch ( err ) { } } function on( elem, types, selector, data, fn, one ) { var origFn, type; // Types can be a map of types/handlers if ( typeof types === "object" ) { // ( types-Object, selector, data ) if ( typeof selector !== "string" ) { // ( types-Object, data ) data = data || selector; selector = undefined; } for ( type in types ) { on( elem, type, selector, data, types[ type ], one ); } return elem; } if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ) fn = data; data = undefined; } else { // ( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { fn = returnFalse; } else if ( !fn ) { return elem; } if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } return elem.each( function() { jQuery.event.add( this, types, fn, data, selector ); } ); } /* * Helper functions for managing events -- not part of the public interface. * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { global: {}, add: function( elem, types, handler, data, selector ) { var tmp, events, t, handleObjIn, special, eventHandle, handleObj, handlers, type, namespaces, origType, elemData = jQuery._data( elem ); // Don't attach events to noData or text/comment nodes (but allow plain objects) if ( !elemData ) { return; } // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; selector = handleObjIn.selector; } // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } // Init the element's event structure and main handler, if this is the first if ( !( events = elemData.events ) ) { events = elemData.events = {}; } if ( !( eventHandle = elemData.handle ) ) { eventHandle = elemData.handle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== "undefined" && ( !e || jQuery.event.triggered !== e.type ) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; // Add elem as a property of the handle fn to prevent a memory leak // with IE non-native events eventHandle.elem = elem; } // Handle multiple events separated by a space types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[ t ] ) || []; type = origType = tmp[ 1 ]; namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); // There *must* be a type, no attaching namespace-only handlers if ( !type ) { continue; } // If event changes its type, use the special event handlers for the changed type special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers handleObj = jQuery.extend( { type: type, origType: origType, data: data, handler: handler, guid: handler.guid, selector: selector, needsContext: selector && jQuery.expr.match.needsContext.test( selector ), namespace: namespaces.join( "." ) }, handleObjIn ); // Init the event handler queue if we're the first if ( !( handlers = events[ type ] ) ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } // Nullify elem to prevent memory leaks in IE elem = null; }, // Detach an event or set of events from an element remove: function( elem, types, handler, selector, mappedTypes ) { var j, handleObj, tmp, origCount, t, events, special, handlers, type, namespaces, origType, elemData = jQuery.hasData( elem ) && jQuery._data( elem ); if ( !elemData || !( events = elemData.events ) ) { return; } // Once for each type.namespace in types; type may be omitted types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[ t ] ) || []; type = origType = tmp[ 1 ]; namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); // Unbind all events (on this namespace, if provided) for the element if ( !type ) { for ( type in events ) { jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } special = jQuery.event.special[ type ] || {}; type = ( selector ? special.delegateType : special.bindType ) || type; handlers = events[ type ] || []; tmp = tmp[ 2 ] && new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); // Remove matching events origCount = j = handlers.length; while ( j-- ) { handleObj = handlers[ j ]; if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !tmp || tmp.test( handleObj.namespace ) ) && ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { handlers.splice( j, 1 ); if ( handleObj.selector ) { handlers.delegateCount--; } if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( origCount && !handlers.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { delete elemData.handle; // removeData also checks for emptiness and clears the expando if empty // so use it instead of delete jQuery._removeData( elem, "events" ); } }, trigger: function( event, data, elem, onlyHandlers ) { var handle, ontype, cur, bubbleType, special, tmp, i, eventPath = [ elem || document ], type = hasOwn.call( event, "type" ) ? event.type : event, namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; cur = tmp = elem = elem || document; // Don't do events on text and comment nodes if ( elem.nodeType === 3 || elem.nodeType === 8 ) { return; } // focus/blur morphs to focusin/out; ensure we're not firing them right now if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { return; } if ( type.indexOf( "." ) > -1 ) { // Namespaced trigger; create a regexp to match event type in handle() namespaces = type.split( "." ); type = namespaces.shift(); namespaces.sort(); } ontype = type.indexOf( ":" ) < 0 && "on" + type; // Caller can pass in a jQuery.Event object, Object, or just an event type string event = event[ jQuery.expando ] ? event : new jQuery.Event( type, typeof event === "object" && event ); // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) event.isTrigger = onlyHandlers ? 2 : 3; event.namespace = namespaces.join( "." ); event.rnamespace = event.namespace ? new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : null; // Clean up the event in case it is being reused event.result = undefined; if ( !event.target ) { event.target = elem; } // Clone any incoming data and prepend the event, creating the handler arg list data = data == null ? [ event ] : jQuery.makeArray( data, [ event ] ); // Allow special events to draw outside the lines special = jQuery.event.special[ type ] || {}; if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { return; } // Determine event propagation path in advance, per W3C events spec (#9951) // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { bubbleType = special.delegateType || type; if ( !rfocusMorph.test( bubbleType + type ) ) { cur = cur.parentNode; } for ( ; cur; cur = cur.parentNode ) { eventPath.push( cur ); tmp = cur; } // Only add window if we got to document (e.g., not plain obj or detached DOM) if ( tmp === ( elem.ownerDocument || document ) ) { eventPath.push( tmp.defaultView || tmp.parentWindow || window ); } } // Fire handlers on the event path i = 0; while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { event.type = i > 1 ? bubbleType : special.bindType || type; // jQuery handler handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } // Native handler handle = ontype && cur[ ontype ]; if ( handle && handle.apply && acceptData( cur ) ) { event.result = handle.apply( cur, data ); if ( event.result === false ) { event.preventDefault(); } } } event.type = type; // If nobody prevented the default action, do it now if ( !onlyHandlers && !event.isDefaultPrevented() ) { if ( ( !special._default || special._default.apply( eventPath.pop(), data ) === false ) && acceptData( elem ) ) { // Call a native DOM method on the target with the same name name as the event. // Can't use an .isFunction() check here because IE6/7 fails that test. // Don't do default actions on window, that's where global variables be (#6170) if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method tmp = elem[ ontype ]; if ( tmp ) { elem[ ontype ] = null; } // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; try { elem[ type ](); } catch ( e ) { // IE<9 dies on focus/blur to hidden element (#1486,#12518) // only reproducible on winXP IE8 native, not IE9 in IE8 mode } jQuery.event.triggered = undefined; if ( tmp ) { elem[ ontype ] = tmp; } } } } return event.result; }, dispatch: function( event ) { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event ); var i, j, ret, matched, handleObj, handlerQueue = [], args = slice.call( arguments ), handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], special = jQuery.event.special[ event.type ] || {}; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[ 0 ] = event; event.delegateTarget = this; // Call the preDispatch hook for the mapped type, and let it bail if desired if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } // Determine handlers handlerQueue = jQuery.event.handlers.call( this, event, handlers ); // Run delegates first; they may want to stop propagation beneath us i = 0; while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem; j = 0; while ( ( handleObj = matched.handlers[ j++ ] ) && !event.isImmediatePropagationStopped() ) { // Triggered event must either 1) have no namespace, or 2) have namespace(s) // a subset or equal to those in the bound event (both can have no namespace). if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || handleObj.handler ).apply( matched.elem, args ); if ( ret !== undefined ) { if ( ( event.result = ret ) === false ) { event.preventDefault(); event.stopPropagation(); } } } } } // Call the postDispatch hook for the mapped type if ( special.postDispatch ) { special.postDispatch.call( this, event ); } return event.result; }, handlers: function( event, handlers ) { var i, matches, sel, handleObj, handlerQueue = [], delegateCount = handlers.delegateCount, cur = event.target; // Support (at least): Chrome, IE9 // Find delegate handlers // Black-hole SVG instance trees (#13180) // // Support: Firefox<=42+ // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) if ( delegateCount && cur.nodeType && ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { /* jshint eqeqeq: false */ for ( ; cur != this; cur = cur.parentNode || this ) { /* jshint eqeqeq: true */ // Don't check non-elements (#13208) // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; // Don't conflict with Object.prototype properties (#13203) sel = handleObj.selector + " "; if ( matches[ sel ] === undefined ) { matches[ sel ] = handleObj.needsContext ? jQuery( sel, this ).index( cur ) > -1 : jQuery.find( sel, this, null, [ cur ] ).length; } if ( matches[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push( { elem: cur, handlers: matches } ); } } } } // Add the remaining (directly-bound) handlers if ( delegateCount < handlers.length ) { handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); } return handlerQueue; }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } // Create a writable copy of the event object and normalize some properties var i, prop, copy, type = event.type, originalEvent = event, fixHook = this.fixHooks[ type ]; if ( !fixHook ) { this.fixHooks[ type ] = fixHook = rmouseEvent.test( type ) ? this.mouseHooks : rkeyEvent.test( type ) ? this.keyHooks : {}; } copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; event = new jQuery.Event( originalEvent ); i = copy.length; while ( i-- ) { prop = copy[ i ]; event[ prop ] = originalEvent[ prop ]; } // Support: IE<9 // Fix target property (#1925) if ( !event.target ) { event.target = originalEvent.srcElement || document; } // Support: Safari 6-8+ // Target should not be a text node (#504, #13143) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } // Support: IE<9 // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) event.metaKey = !!event.metaKey; return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; }, // Includes some event props shared by KeyEvent and MouseEvent props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split( " " ), filter: function( event, original ) { // Add which for key events if ( event.which == null ) { event.which = original.charCode != null ? original.charCode : original.keyCode; } return event; } }, mouseHooks: { props: ( "button buttons clientX clientY fromElement offsetX offsetY " + "pageX pageY screenX screenY toElement" ).split( " " ), filter: function( event, original ) { var body, eventDoc, doc, button = original.button, fromElement = original.fromElement; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && original.clientX != null ) { eventDoc = event.target.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } // Add relatedTarget, if necessary if ( !event.relatedTarget && fromElement ) { event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; } // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && button !== undefined ) { event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event; } }, special: { load: { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { // Fire native event if possible so blur/focus sequence is correct trigger: function() { if ( this !== safeActiveElement() && this.focus ) { try { this.focus(); return false; } catch ( e ) { // Support: IE<9 // If we error on focus to hidden element (#1486, #12518), // let .trigger() run the handlers } } }, delegateType: "focusin" }, blur: { trigger: function() { if ( this === safeActiveElement() && this.blur ) { this.blur(); return false; } }, delegateType: "focusout" }, click: { // For checkbox, fire native event so checked state will be right trigger: function() { if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { this.click(); return false; } }, // For cross-browser consistency, don't fire native .click() on links _default: function( event ) { return jQuery.nodeName( event.target, "a" ); } }, beforeunload: { postDispatch: function( event ) { // Support: Firefox 20+ // Firefox doesn't alert if the returnValue field is not set. if ( event.result !== undefined && event.originalEvent ) { event.originalEvent.returnValue = event.result; } } } }, // Piggyback on a donor event to simulate a different one simulate: function( type, elem, event ) { var e = jQuery.extend( new jQuery.Event(), event, { type: type, isSimulated: true // Previously, `originalEvent: {}` was set here, so stopPropagation call // would not be triggered on donor event, since in our own // jQuery.event.stopPropagation function we had a check for existence of // originalEvent.stopPropagation method, so, consequently it would be a noop. // // Guard for simulated events was moved to jQuery.event.stopPropagation function // since `originalEvent` should point to the original event for the // constancy with other events and for more focused logic } ); jQuery.event.trigger( e, null, elem ); if ( e.isDefaultPrevented() ) { event.preventDefault(); } } }; jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { // This "if" is needed for plain objects if ( elem.removeEventListener ) { elem.removeEventListener( type, handle ); } } : function( elem, type, handle ) { var name = "on" + type; if ( elem.detachEvent ) { // #8545, #7054, preventing memory leaks for custom events in IE6-8 // detachEvent needed property on element, by name of that event, // to properly expose it to GC if ( typeof elem[ name ] === "undefined" ) { elem[ name ] = null; } elem.detachEvent( name, handle ); } }; jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword if ( !( this instanceof jQuery.Event ) ) { return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. this.isDefaultPrevented = src.defaultPrevented || src.defaultPrevented === undefined && // Support: IE < 9, Android < 4.0 src.returnValue === false ? returnTrue : returnFalse; // Event type } else { this.type = src; } // Put explicitly provided properties onto the event object if ( props ) { jQuery.extend( this, props ); } // Create a timestamp if incoming event doesn't have one this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; }; // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { constructor: jQuery.Event, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse, preventDefault: function() { var e = this.originalEvent; this.isDefaultPrevented = returnTrue; if ( !e ) { return; } // If preventDefault exists, run it on the original event if ( e.preventDefault ) { e.preventDefault(); // Support: IE // Otherwise set the returnValue property of the original event to false } else { e.returnValue = false; } }, stopPropagation: function() { var e = this.originalEvent; this.isPropagationStopped = returnTrue; if ( !e || this.isSimulated ) { return; } // If stopPropagation exists, run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } // Support: IE // Set the cancelBubble property of the original event to true e.cancelBubble = true; }, stopImmediatePropagation: function() { var e = this.originalEvent; this.isImmediatePropagationStopped = returnTrue; if ( e && e.stopImmediatePropagation ) { e.stopImmediatePropagation(); } this.stopPropagation(); } }; // Create mouseenter/leave events using mouseover/out and event-time checks // so that event delegation works in jQuery. // Do the same for pointerenter/pointerleave and pointerover/pointerout // // Support: Safari 7 only // Safari sends mouseenter too often; see: // https://code.google.com/p/chromium/issues/detail?id=470258 // for the description of the bug (it existed in older Chrome versions as well). jQuery.each( { mouseenter: "mouseover", mouseleave: "mouseout", pointerenter: "pointerover", pointerleave: "pointerout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, bindType: fix, handle: function( event ) { var ret, target = this, related = event.relatedTarget, handleObj = event.handleObj; // For mouseenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; } return ret; } }; } ); // IE submit delegation if ( !support.submit ) { jQuery.event.special.submit = { setup: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Lazy-add a submit handler when a descendant form may potentially be submitted jQuery.event.add( this, "click._submit keypress._submit", function( e ) { // Node name check avoids a VML-related crash in IE (#9807) var elem = e.target, form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? // Support: IE <=8 // We use jQuery.prop instead of elem.form // to allow fixing the IE8 delegated submit issue (gh-2332) // by 3rd party polyfills/workarounds. jQuery.prop( elem, "form" ) : undefined; if ( form && !jQuery._data( form, "submit" ) ) { jQuery.event.add( form, "submit._submit", function( event ) { event._submitBubble = true; } ); jQuery._data( form, "submit", true ); } } ); // return undefined since we don't need an event listener }, postDispatch: function( event ) { // If form was submitted by the user, bubble the event up the tree if ( event._submitBubble ) { delete event._submitBubble; if ( this.parentNode && !event.isTrigger ) { jQuery.event.simulate( "submit", this.parentNode, event ); } } }, teardown: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Remove delegated handlers; cleanData eventually reaps submit handlers attached above jQuery.event.remove( this, "._submit" ); } }; } // IE change delegation and checkbox/radio fix if ( !support.change ) { jQuery.event.special.change = { setup: function() { if ( rformElems.test( this.nodeName ) ) { // IE doesn't fire change on a check/radio until blur; trigger it on click // after a propertychange. Eat the blur-change in special.change.handle. // This still fires onchange a second time for check/radio after blur. if ( this.type === "checkbox" || this.type === "radio" ) { jQuery.event.add( this, "propertychange._change", function( event ) { if ( event.originalEvent.propertyName === "checked" ) { this._justChanged = true; } } ); jQuery.event.add( this, "click._change", function( event ) { if ( this._justChanged && !event.isTrigger ) { this._justChanged = false; } // Allow triggered, simulated change events (#11500) jQuery.event.simulate( "change", this, event ); } ); } return false; } // Delegated event; lazy-add a change handler on descendant inputs jQuery.event.add( this, "beforeactivate._change", function( e ) { var elem = e.target; if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "change" ) ) { jQuery.event.add( elem, "change._change", function( event ) { if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { jQuery.event.simulate( "change", this.parentNode, event ); } } ); jQuery._data( elem, "change", true ); } } ); }, handle: function( event ) { var elem = event.target; // Swallow native change events from checkbox/radio, we already triggered them above if ( this !== elem || event.isSimulated || event.isTrigger || ( elem.type !== "radio" && elem.type !== "checkbox" ) ) { return event.handleObj.handler.apply( this, arguments ); } }, teardown: function() { jQuery.event.remove( this, "._change" ); return !rformElems.test( this.nodeName ); } }; } // Support: Firefox // Firefox doesn't have focus(in | out) events // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 // // Support: Chrome, Safari // focus(in | out) events fire after focus & blur events, // which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order // Related ticket - https://code.google.com/p/chromium/issues/detail?id=449857 if ( !support.focusin ) { jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { // Attach a single capturing handler on the document while someone wants focusin/focusout var handler = function( event ) { jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); }; jQuery.event.special[ fix ] = { setup: function() { var doc = this.ownerDocument || this, attaches = jQuery._data( doc, fix ); if ( !attaches ) { doc.addEventListener( orig, handler, true ); } jQuery._data( doc, fix, ( attaches || 0 ) + 1 ); }, teardown: function() { var doc = this.ownerDocument || this, attaches = jQuery._data( doc, fix ) - 1; if ( !attaches ) { doc.removeEventListener( orig, handler, true ); jQuery._removeData( doc, fix ); } else { jQuery._data( doc, fix, attaches ); } } }; } ); } jQuery.fn.extend( { on: function( types, selector, data, fn ) { return on( this, types, selector, data, fn ); }, one: function( types, selector, data, fn ) { return on( this, types, selector, data, fn, 1 ); }, off: function( types, selector, fn ) { var handleObj, type; if ( types && types.preventDefault && types.handleObj ) { // ( event ) dispatched jQuery.Event handleObj = types.handleObj; jQuery( types.delegateTarget ).off( handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, handleObj.selector, handleObj.handler ); return this; } if ( typeof types === "object" ) { // ( types-object [, selector] ) for ( type in types ) { this.off( type, selector, types[ type ] ); } return this; } if ( selector === false || typeof selector === "function" ) { // ( types [, fn] ) fn = selector; selector = undefined; } if ( fn === false ) { fn = returnFalse; } return this.each( function() { jQuery.event.remove( this, types, fn, selector ); } ); }, trigger: function( type, data ) { return this.each( function() { jQuery.event.trigger( type, data, this ); } ); }, triggerHandler: function( type, data ) { var elem = this[ 0 ]; if ( elem ) { return jQuery.event.trigger( type, data, elem, true ); } } } ); var rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, rnoshimcache = new RegExp( "<(?:" + nodeNames + ")[\\s/>]", "i" ), rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, // Support: IE 10-11, Edge 10240+ // In IE/Edge using regex groups here causes severe slowdowns. // See https://connect.microsoft.com/IE/feedback/details/1736512/ rnoInnerhtml = /\s*$/g, safeFragment = createSafeFragment( document ), fragmentDiv = safeFragment.appendChild( document.createElement( "div" ) ); // Support: IE<8 // Manipulating tables requires a tbody function manipulationTarget( elem, content ) { return jQuery.nodeName( elem, "table" ) && jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? elem.getElementsByTagName( "tbody" )[ 0 ] || elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) : elem; } // Replace/restore the type attribute of script elements for safe DOM manipulation function disableScript( elem ) { elem.type = ( jQuery.find.attr( elem, "type" ) !== null ) + "/" + elem.type; return elem; } function restoreScript( elem ) { var match = rscriptTypeMasked.exec( elem.type ); if ( match ) { elem.type = match[ 1 ]; } else { elem.removeAttribute( "type" ); } return elem; } function cloneCopyEvent( src, dest ) { if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { return; } var type, i, l, oldData = jQuery._data( src ), curData = jQuery._data( dest, oldData ), events = oldData.events; if ( events ) { delete curData.handle; curData.events = {}; for ( type in events ) { for ( i = 0, l = events[ type ].length; i < l; i++ ) { jQuery.event.add( dest, type, events[ type ][ i ] ); } } } // make the cloned public data object a copy from the original if ( curData.data ) { curData.data = jQuery.extend( {}, curData.data ); } } function fixCloneNodeIssues( src, dest ) { var nodeName, e, data; // We do not need to do anything for non-Elements if ( dest.nodeType !== 1 ) { return; } nodeName = dest.nodeName.toLowerCase(); // IE6-8 copies events bound via attachEvent when using cloneNode. if ( !support.noCloneEvent && dest[ jQuery.expando ] ) { data = jQuery._data( dest ); for ( e in data.events ) { jQuery.removeEvent( dest, e, data.handle ); } // Event data gets referenced instead of copied if the expando gets copied too dest.removeAttribute( jQuery.expando ); } // IE blanks contents when cloning scripts, and tries to evaluate newly-set text if ( nodeName === "script" && dest.text !== src.text ) { disableScript( dest ).text = src.text; restoreScript( dest ); // IE6-10 improperly clones children of object elements using classid. // IE10 throws NoModificationAllowedError if parent is null, #12132. } else if ( nodeName === "object" ) { if ( dest.parentNode ) { dest.outerHTML = src.outerHTML; } // This path appears unavoidable for IE9. When cloning an object // element in IE9, the outerHTML strategy above is not sufficient. // If the src has innerHTML and the destination does not, // copy the src.innerHTML into the dest.innerHTML. #10324 if ( support.html5Clone && ( src.innerHTML && !jQuery.trim( dest.innerHTML ) ) ) { dest.innerHTML = src.innerHTML; } } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { // IE6-8 fails to persist the checked state of a cloned checkbox // or radio button. Worse, IE6-7 fail to give the cloned element // a checked appearance if the defaultChecked value isn't also set dest.defaultChecked = dest.checked = src.checked; // IE6-7 get confused and end up setting the value of a cloned // checkbox/radio button to an empty string instead of "on" if ( dest.value !== src.value ) { dest.value = src.value; } // IE6-8 fails to return the selected option to the default selected // state when cloning options } else if ( nodeName === "option" ) { dest.defaultSelected = dest.selected = src.defaultSelected; // IE6-8 fails to set the defaultValue to the correct value when // cloning other types of input fields } else if ( nodeName === "input" || nodeName === "textarea" ) { dest.defaultValue = src.defaultValue; } } function domManip( collection, args, callback, ignored ) { // Flatten any nested arrays args = concat.apply( [], args ); var first, node, hasScripts, scripts, doc, fragment, i = 0, l = collection.length, iNoClone = l - 1, value = args[ 0 ], isFunction = jQuery.isFunction( value ); // We can't cloneNode fragments that contain checked, in WebKit if ( isFunction || ( l > 1 && typeof value === "string" && !support.checkClone && rchecked.test( value ) ) ) { return collection.each( function( index ) { var self = collection.eq( index ); if ( isFunction ) { args[ 0 ] = value.call( this, index, self.html() ); } domManip( self, args, callback, ignored ); } ); } if ( l ) { fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); first = fragment.firstChild; if ( fragment.childNodes.length === 1 ) { fragment = first; } // Require either new content or an interest in ignored elements to invoke the callback if ( first || ignored ) { scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); hasScripts = scripts.length; // Use the original fragment for the last item // instead of the first because it can end up // being emptied incorrectly in certain situations (#8070). for ( ; i < l; i++ ) { node = fragment; if ( i !== iNoClone ) { node = jQuery.clone( node, true, true ); // Keep references to cloned scripts for later restoration if ( hasScripts ) { // Support: Android<4.1, PhantomJS<2 // push.apply(_, arraylike) throws on ancient WebKit jQuery.merge( scripts, getAll( node, "script" ) ); } } callback.call( collection[ i ], node, i ); } if ( hasScripts ) { doc = scripts[ scripts.length - 1 ].ownerDocument; // Reenable scripts jQuery.map( scripts, restoreScript ); // Evaluate executable scripts on first document insertion for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { if ( node.src ) { // Optional AJAX dependency, but won't run scripts if not present if ( jQuery._evalUrl ) { jQuery._evalUrl( node.src ); } } else { jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ) .replace( rcleanScript, "" ) ); } } } } // Fix #11809: Avoid leaking memory fragment = first = null; } } return collection; } function remove( elem, selector, keepData ) { var node, elems = selector ? jQuery.filter( selector, elem ) : elem, i = 0; for ( ; ( node = elems[ i ] ) != null; i++ ) { if ( !keepData && node.nodeType === 1 ) { jQuery.cleanData( getAll( node ) ); } if ( node.parentNode ) { if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { setGlobalEval( getAll( node, "script" ) ); } node.parentNode.removeChild( node ); } } return elem; } jQuery.extend( { htmlPrefilter: function( html ) { return html.replace( rxhtmlTag, "<$1>" ); }, clone: function( elem, dataAndEvents, deepDataAndEvents ) { var destElements, node, clone, i, srcElements, inPage = jQuery.contains( elem.ownerDocument, elem ); if ( support.html5Clone || jQuery.isXMLDoc( elem ) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { clone = elem.cloneNode( true ); // IE<=8 does not properly clone detached, unknown element nodes } else { fragmentDiv.innerHTML = elem.outerHTML; fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); } if ( ( !support.noCloneEvent || !support.noCloneChecked ) && ( elem.nodeType === 1 || elem.nodeType === 11 ) && !jQuery.isXMLDoc( elem ) ) { // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 destElements = getAll( clone ); srcElements = getAll( elem ); // Fix all IE cloning issues for ( i = 0; ( node = srcElements[ i ] ) != null; ++i ) { // Ensure that the destination node is not null; Fixes #9587 if ( destElements[ i ] ) { fixCloneNodeIssues( node, destElements[ i ] ); } } } // Copy the events from the original to the clone if ( dataAndEvents ) { if ( deepDataAndEvents ) { srcElements = srcElements || getAll( elem ); destElements = destElements || getAll( clone ); for ( i = 0; ( node = srcElements[ i ] ) != null; i++ ) { cloneCopyEvent( node, destElements[ i ] ); } } else { cloneCopyEvent( elem, clone ); } } // Preserve script evaluation history destElements = getAll( clone, "script" ); if ( destElements.length > 0 ) { setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); } destElements = srcElements = node = null; // Return the cloned set return clone; }, cleanData: function( elems, /* internal */ forceAcceptData ) { var elem, type, id, data, i = 0, internalKey = jQuery.expando, cache = jQuery.cache, attributes = support.attributes, special = jQuery.event.special; for ( ; ( elem = elems[ i ] ) != null; i++ ) { if ( forceAcceptData || acceptData( elem ) ) { id = elem[ internalKey ]; data = id && cache[ id ]; if ( data ) { if ( data.events ) { for ( type in data.events ) { if ( special[ type ] ) { jQuery.event.remove( elem, type ); // This is a shortcut to avoid jQuery.event.remove's overhead } else { jQuery.removeEvent( elem, type, data.handle ); } } } // Remove cache only if it was not already removed by jQuery.event.remove if ( cache[ id ] ) { delete cache[ id ]; // Support: IE<9 // IE does not allow us to delete expando properties from nodes // IE creates expando attributes along with the property // IE does not have a removeAttribute function on Document nodes if ( !attributes && typeof elem.removeAttribute !== "undefined" ) { elem.removeAttribute( internalKey ); // Webkit & Blink performance suffers when deleting properties // from DOM nodes, so set to undefined instead // https://code.google.com/p/chromium/issues/detail?id=378607 } else { elem[ internalKey ] = undefined; } deletedIds.push( id ); } } } } } } ); jQuery.fn.extend( { // Keep domManip exposed until 3.0 (gh-2225) domManip: domManip, detach: function( selector ) { return remove( this, selector, true ); }, remove: function( selector ) { return remove( this, selector ); }, text: function( value ) { return access( this, function( value ) { return value === undefined ? jQuery.text( this ) : this.empty().append( ( this[ 0 ] && this[ 0 ].ownerDocument || document ).createTextNode( value ) ); }, null, value, arguments.length ); }, append: function() { return domManip( this, arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.appendChild( elem ); } } ); }, prepend: function() { return domManip( this, arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.insertBefore( elem, target.firstChild ); } } ); }, before: function() { return domManip( this, arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this ); } } ); }, after: function() { return domManip( this, arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this.nextSibling ); } } ); }, empty: function() { var elem, i = 0; for ( ; ( elem = this[ i ] ) != null; i++ ) { // Remove element nodes and prevent memory leaks if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); } // Remove any remaining nodes while ( elem.firstChild ) { elem.removeChild( elem.firstChild ); } // If this is a select, ensure that it displays empty (#12336) // Support: IE<9 if ( elem.options && jQuery.nodeName( elem, "select" ) ) { elem.options.length = 0; } } return this; }, clone: function( dataAndEvents, deepDataAndEvents ) { dataAndEvents = dataAndEvents == null ? false : dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; return this.map( function() { return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); } ); }, html: function( value ) { return access( this, function( value ) { var elem = this[ 0 ] || {}, i = 0, l = this.length; if ( value === undefined ) { return elem.nodeType === 1 ? elem.innerHTML.replace( rinlinejQuery, "" ) : undefined; } // See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && ( support.htmlSerialize || !rnoshimcache.test( value ) ) && ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { value = jQuery.htmlPrefilter( value ); try { for ( ; i < l; i++ ) { // Remove element nodes and prevent memory leaks elem = this[ i ] || {}; if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; } } elem = 0; // If using innerHTML throws an exception, use the fallback method } catch ( e ) {} } if ( elem ) { this.empty().append( value ); } }, null, value, arguments.length ); }, replaceWith: function() { var ignored = []; // Make the changes, replacing each non-ignored context element with the new content return domManip( this, arguments, function( elem ) { var parent = this.parentNode; if ( jQuery.inArray( this, ignored ) < 0 ) { jQuery.cleanData( getAll( this ) ); if ( parent ) { parent.replaceChild( elem, this ); } } // Force callback invocation }, ignored ); } } ); jQuery.each( { appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var elems, i = 0, ret = [], insert = jQuery( selector ), last = insert.length - 1; for ( ; i <= last; i++ ) { elems = i === last ? this : this.clone( true ); jQuery( insert[ i ] )[ original ]( elems ); // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() push.apply( ret, elems.get() ); } return this.pushStack( ret ); }; } ); var iframe, elemdisplay = { // Support: Firefox // We have to pre-define these values for FF (#10227) HTML: "block", BODY: "block" }; /** * Retrieve the actual display of a element * @param {String} name nodeName of the element * @param {Object} doc Document object */ // Called only from within defaultDisplay function actualDisplay( name, doc ) { var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), display = jQuery.css( elem[ 0 ], "display" ); // We don't have any data stored on the element, // so use "detach" method as fast way to get rid of the element elem.detach(); return display; } /** * Try to determine the default display value of an element * @param {String} nodeName */ function defaultDisplay( nodeName ) { var doc = document, display = elemdisplay[ nodeName ]; if ( !display ) { display = actualDisplay( nodeName, doc ); // If the simple way fails, read from inside an iframe if ( display === "none" || !display ) { // Use the already-created iframe if possible iframe = ( iframe || jQuery( "