├── .babelrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── package.json
├── src
├── constants.js
├── gitment.js
├── icons.js
├── test.js
├── theme
│ └── default.js
└── utils.js
├── style
└── default.css
├── test
├── gitment.browser.html
├── gitment.html
└── style.css
├── webpack.config.js
└── webpack.dev.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 |
4 | node_modules
5 |
6 | dist
7 | test/config.js
8 | index.html
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .gitignore
4 | .npmignore
5 | .babelrc
6 |
7 | node_modules
8 |
9 | src
10 | test
11 | index.html
12 | webpack.config.js
13 | webpack.dev.config.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 imsun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gitment
2 |
3 |
4 | [![NPM version][npm-image]][npm-url]
5 |
6 | [npm-image]: https://img.shields.io/npm/v/gitment.svg
7 | [npm-url]: https://www.npmjs.com/package/gitment
8 |
9 | Gitment is a comment system based on GitHub Issues,
10 | which can be used in the frontend without any server-side implementation.
11 |
12 | [Demo Page](https://imsun.github.io/gitment/)
13 |
14 | [中文简介](https://imsun.net/posts/gitment-introduction/)
15 |
16 | - [Features](#features)
17 | - [Get Started](#get-started)
18 | - [Methods](#methods)
19 | - [Customize](#customize)
20 | - [About Security](#about-security)
21 |
22 | ## Features
23 |
24 | - GitHub Login
25 | - Markdown / GFM support
26 | - Syntax highlighting
27 | - Notifications from GitHub
28 | - Easy to customize
29 | - No server-side implementation
30 |
31 | ## Get Started
32 |
33 | ### 1. Install
34 |
35 | ```html
36 |
37 | ```
38 |
39 | ```html
40 |
41 | ```
42 |
43 | or via npm:
44 |
45 | ```sh
46 | $ npm i --save gitment
47 | ```
48 |
49 | ```javascript
50 | import 'gitment/style/default.css'
51 | import Gitment from 'gitment'
52 | ```
53 |
54 | ### 2. Register An OAuth Application
55 |
56 | [Click here](https://github.com/settings/applications/new) to register an OAuth application, and you will get a client ID and a client secret.
57 |
58 | Make sure the callback URL is right. Generally it's the origin of your site, like [https://imsun.net](https://imsun.net).
59 |
60 | ### 3. Render Gitment
61 |
62 | ```javascript
63 | const gitment = new Gitment({
64 | id: 'Your page ID', // optional
65 | owner: 'Your GitHub ID',
66 | repo: 'The repo to store comments',
67 | oauth: {
68 | client_id: 'Your client ID',
69 | client_secret: 'Your client secret',
70 | },
71 | // ...
72 | // For more available options, check out the documentation below
73 | })
74 |
75 | gitment.render('comments')
76 | // or
77 | // gitment.render(document.getElementById('comments'))
78 | // or
79 | // document.body.appendChild(gitment.render())
80 | ```
81 |
82 | ### 4. Initialize Your Comments
83 |
84 | After the page is published, you should visit your page, login with your GitHub account(make sure you're repo's owner), and click the initialize button, to create a related issue in your repo.
85 | After that, others can leave their comments.
86 |
87 | ## Methods
88 |
89 | ### constructor(options)
90 |
91 | #### options:
92 |
93 | Type: `object`
94 |
95 | - owner: Your GitHub ID. Required.
96 | - repo: The repository to store your comments. Make sure you're repo's owner. Required.
97 | - oauth: An object contains your client ID and client secret. Required.
98 | - client_id: GitHub client ID. Required.
99 | - client_secret: GitHub client secret. Required.
100 | - id: An optional string to identify your page. Default `location.href`.
101 | - title: An optional title for your page, used as issue's title. Default `document.title`.
102 | - link: An optional link for your page, used in issue's body. Default `location.href`.
103 | - desc: An optional description for your page, used in issue's body. Default `''`.
104 | - labels: An optional array of labels your want to add when creating the issue. Default `[]`.
105 | - theme: An optional Gitment theme object. Default `gitment.defaultTheme`.
106 | - perPage: An optional number to which comments will be paginated. Default `20`.
107 | - maxCommentHeight: An optional number to limit comments' max height, over which comments will be folded. Default `250`.
108 |
109 | ### gitment.render([element])
110 |
111 | #### element
112 |
113 | Type: `HTMLElement` or `string`
114 |
115 | The DOM element to which comments will be rendered. Can be an HTML element or element's id. When omitted, this function will create a new `div` element.
116 |
117 | This function returns the element to which comments be rendered.
118 |
119 | ### gitment.renderHeader([element])
120 |
121 | Same like `gitment.render([element])`. But only renders the header.
122 |
123 | ### gitment.renderComments([element])
124 |
125 | Same like `gitment.render([element])`. But only renders comments list.
126 |
127 |
128 | ### gitment.renderEditor([element])
129 |
130 | Same like `gitment.render([element])`. But only renders the editor.
131 |
132 |
133 | ### gitment.renderFooter([element])
134 |
135 | Same like `gitment.render([element])`. But only renders the footer.
136 |
137 | ### gitment.init()
138 |
139 | Initialize a new page. Returns a `Promise` and resolves when initialized.
140 |
141 | ### gitment.update()
142 |
143 | Update data and views. Returns a `Promise` and resolves when data updated.
144 |
145 | ### gitment.post()
146 |
147 | Post comment in the editor. Returns a `Promise` and resolves when posted.
148 |
149 | ### gitment.markdown(text)
150 |
151 | #### text
152 |
153 | Type: `string`
154 |
155 | Returns a `Promise` and resolves rendered text.
156 |
157 | ### gitment.login()
158 |
159 | Jump to GitHub OAuth page to login.
160 |
161 | ### gitment.logout()
162 |
163 | Log out current user.
164 |
165 | ### goto(page)
166 |
167 | #### page
168 |
169 | Type: `number`
170 |
171 | Jump to the target page of comments. Notice that `page` starts from `1`. Returns a `Promise` and resolves when comments loaded.
172 |
173 | ### gitment.like()
174 |
175 | Like current page. Returns a `Promise` and resolves when liked.
176 |
177 | ### gitment.unlike()
178 |
179 | Unlike current page. Returns a `Promise` and resolves when unliked.
180 |
181 | ### gitment.likeAComment(commentId)
182 |
183 | #### commentId
184 |
185 | Type: `string`
186 |
187 | Like a comment. Returns a `Promise` and resolves when liked.
188 |
189 | ### gitment.unlikeAComment(commentId)
190 |
191 | #### commentId
192 |
193 | Type: `string`
194 |
195 | Unlike a comment. Returns a `Promise` and resolves when unliked.
196 |
197 | ## Customize
198 |
199 | Gitment is easy to customize. You can use your own CSS or write a theme.
200 | (The difference is that customized CSS can't modify DOM structure)
201 |
202 | ### Use Customized CSS
203 |
204 | Gitment does't use any atomic CSS, making it easier and more flexible to customize.
205 | You can inspect the DOM structure in the browser and write your own styles.
206 |
207 | ### Write A Theme
208 |
209 | A Gitment theme is an object contains several render functions.
210 |
211 | By default Gitment has five render functions: `render`, `renderHeader`, `renderComments`, `renderEditor`, `renderFooter`.
212 | The last four render independent components and `render` functions render them together.
213 | All of them can be used independently.
214 |
215 | You can override any render function above or write your own render function.
216 |
217 | For example, you can override the `render` function to put an editor before the comment list, and render a new component.
218 |
219 | ```javascript
220 | const myTheme = {
221 | render(state, instance) {
222 | const container = document.createElement('div')
223 | container.lang = "en-US"
224 | container.className = 'gitment-container gitment-root-container'
225 |
226 | // your custom component
227 | container.appendChild(instance.renderSomething(state, instance))
228 |
229 | container.appendChild(instance.renderHeader(state, instance))
230 | container.appendChild(instance.renderEditor(state, instance))
231 | container.appendChild(instance.renderComments(state, instance))
232 | container.appendChild(instance.renderFooter(state, instance))
233 | return container
234 | },
235 | renderSomething(state, instance) {
236 | const container = document.createElement('div')
237 | container.lang = "en-US"
238 | if (state.user.login) {
239 | container.innerText = `Hello, ${state.user.login}`
240 | }
241 | return container
242 | }
243 | }
244 |
245 | const gitment = new Gitment({
246 | // ...
247 | theme: myTheme,
248 | })
249 |
250 | gitment.render(document.body)
251 | // or
252 | // gitment.renderSomthing(document.body)
253 | ```
254 |
255 | Each render function should receive a state object and a gitment instance, and return an HTML element.
256 | It will be wrapped attached to the Gitment instance with the same name.
257 |
258 | Gitment uses [MobX](https://github.com/mobxjs/mobx) to detect states used in render functions.
259 | Once used states change, Gitment will call the render function to get a new element and render it.
260 | Unused states' changing won't affect rendered elements.
261 |
262 | Available states:
263 |
264 | - user: `object`. User info returned from [GitHub Users API](https://developer.github.com/v3/users/#get-the-authenticated-user) with two more keys.
265 | - isLoggingIn: `bool`. Indicates if user is logging in.
266 | - fromCache: `bool`. Gitment will cache user's information. Its value indicates if current user info is from cache.
267 | - error: `Error Object`. Will be null if no error occurs.
268 | - meta: `object`. Issue's info returned from [GitHub Issues API](https://developer.github.com/v3/issues/#list-issues).
269 | - comments: `array`. Array of comment returned from [GitHub Issue Comments API](/repos/:owner/:repo/issues/:number/comments). Will be `undefined` when comments not loaded.
270 | - reactions: `array`. Array of reactions added to current page, returned from [GitHub Issues' Reactions API](https://developer.github.com/v3/reactions/#list-reactions-for-an-issue).
271 | - commentReactions: `object`. Object of reactions added to comments, with comment ID as key, returned from [GitHub Issue Comments' Reactions API](/repos/:owner/:repo/issues/comments/:id/reactions).
272 | - currentPage: `number`. Which page of comments is user on. Starts from `1`.
273 |
274 | ## About Security
275 |
276 | ### Is it safe to make my client secret public?
277 |
278 | Client secret is necessary for OAuth, without which users can't login or comment with their GitHub accounts.
279 | Although GitHub does't recommend to hard code client secret in the frontend, you can still do that because GitHub will verify your callback URL.
280 | In theory, no one else can use your secret except your site.
281 |
282 | If you find a way to hack it, please [open an issue](https://github.com/imsun/gitment/issues/new).
283 |
284 | ### Why does Gitment send a request to gh-oauth.imsun.net?
285 |
286 | [https://gh-oauth.imsun.net](https://gh-oauth.imsun.net) is an simple open-source service to proxy [one request](https://developer.github.com/v3/oauth/#2-github-redirects-back-to-your-site) during users logging in.
287 | Because GitHub doesn't attach a CORS header to it.
288 |
289 | This service won't record or store anything. It only attaches a CORS header to that request and provides proxy.
290 | So that users can login in the frontend without any server-side implementation.
291 |
292 | For more details, checkout [this project](https://github.com/imsun/gh-oauth-server).
293 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitment",
3 | "version": "0.0.3",
4 | "description": "A comment system based on GitHub Issues",
5 | "main": "./dist/gitment.js",
6 | "author": {
7 | "name": "Shiquan Sun",
8 | "url": "https://github.com/imsun"
9 | },
10 | "keywords": [
11 | "comment system",
12 | "GitHub Issues"
13 | ],
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/imsun/gitment"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/imsun/gitment/issues"
20 | },
21 | "homepage": "https://github.com/imsun/gitment",
22 | "scripts": {
23 | "build": "babel src --out-dir dist --ignore test.js --source-maps & NODE_ENV=production webpack --config webpack.config.js --progress --profile --colors",
24 | "dev": "webpack-dev-server --config webpack.dev.config.js --host 0.0.0.0 --progress --profile --colors"
25 | },
26 | "devDependencies": {
27 | "babel-cli": "^6.24.0",
28 | "babel-core": "^6.24.0",
29 | "babel-loader": "^6.4.1",
30 | "babel-preset-es2015": "^6.24.0",
31 | "webpack": "^2.3.2",
32 | "webpack-dev-server": "^2.4.2"
33 | },
34 | "dependencies": {
35 | "mobx": "^3.1.7"
36 | },
37 | "license": "MIT"
38 | }
39 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const LS_ACCESS_TOKEN_KEY = 'gitment-comments-token'
2 | export const LS_USER_KEY = 'gitment-user-info'
3 |
4 | export const NOT_INITIALIZED_ERROR = new Error('Comments Not Initialized')
5 |
--------------------------------------------------------------------------------
/src/gitment.js:
--------------------------------------------------------------------------------
1 | import { autorun, observable } from 'mobx'
2 |
3 | import { LS_ACCESS_TOKEN_KEY, LS_USER_KEY, NOT_INITIALIZED_ERROR } from './constants'
4 | import { getTargetContainer, http, Query } from './utils'
5 | import defaultTheme from './theme/default'
6 |
7 | const scope = 'public_repo'
8 |
9 | function extendRenderer(instance, renderer) {
10 | instance[renderer] = (container) => {
11 | const targetContainer = getTargetContainer(container)
12 | const render = instance.theme[renderer] || instance.defaultTheme[renderer]
13 |
14 | autorun(() => {
15 | const e = render(instance.state, instance)
16 | if (targetContainer.firstChild) {
17 | targetContainer.replaceChild(e, targetContainer.firstChild)
18 | } else {
19 | targetContainer.appendChild(e)
20 | }
21 | })
22 |
23 | return targetContainer
24 | }
25 | }
26 |
27 | class Gitment {
28 | get accessToken() {
29 | return localStorage.getItem(LS_ACCESS_TOKEN_KEY)
30 | }
31 | set accessToken(token) {
32 | localStorage.setItem(LS_ACCESS_TOKEN_KEY, token)
33 | }
34 |
35 | get loginLink() {
36 | const oauthUri = 'https://github.com/login/oauth/authorize'
37 | const redirect_uri = this.oauth.redirect_uri || window.location.href
38 |
39 | const oauthParams = Object.assign({
40 | scope,
41 | redirect_uri,
42 | }, this.oauth)
43 |
44 | return `${oauthUri}${Query.stringify(oauthParams)}`
45 | }
46 |
47 | constructor(options = {}) {
48 | this.defaultTheme = defaultTheme
49 | this.useTheme(defaultTheme)
50 |
51 | Object.assign(this, {
52 | id: window.location.href,
53 | title: window.document.title,
54 | link: window.location.href,
55 | desc: '',
56 | labels: [],
57 | theme: defaultTheme,
58 | oauth: {},
59 | perPage: 20,
60 | maxCommentHeight: 250,
61 | }, options)
62 |
63 | this.useTheme(this.theme)
64 |
65 | const user = {}
66 | try {
67 | const userInfo = localStorage.getItem(LS_USER_KEY)
68 | if (this.accessToken && userInfo) {
69 | Object.assign(user, JSON.parse(userInfo), {
70 | fromCache: true,
71 | })
72 | }
73 | } catch (e) {
74 | localStorage.removeItem(LS_USER_KEY)
75 | }
76 |
77 | this.state = observable({
78 | user,
79 | error: null,
80 | meta: {},
81 | comments: undefined,
82 | reactions: [],
83 | commentReactions: {},
84 | currentPage: 1,
85 | })
86 |
87 | const query = Query.parse()
88 | if (query.code) {
89 | const { client_id, client_secret } = this.oauth
90 | const code = query.code
91 | delete query.code
92 | const search = Query.stringify(query)
93 | const replacedUrl = `${window.location.origin}${window.location.pathname}${search}${window.location.hash}`
94 | history.replaceState({}, '', replacedUrl)
95 |
96 | Object.assign(this, {
97 | id: replacedUrl,
98 | link: replacedUrl,
99 | }, options)
100 |
101 | this.state.user.isLoggingIn = true
102 | http.post('https://gh-oauth.imsun.net', {
103 | code,
104 | client_id,
105 | client_secret,
106 | }, '')
107 | .then(data => {
108 | this.accessToken = data.access_token
109 | this.update()
110 | })
111 | .catch(e => {
112 | this.state.user.isLoggingIn = false
113 | alert(e)
114 | })
115 | } else {
116 | this.update()
117 | }
118 | }
119 |
120 | init() {
121 | return this.createIssue()
122 | .then(() => this.loadComments())
123 | .then(comments => {
124 | this.state.error = null
125 | return comments
126 | })
127 | }
128 |
129 | useTheme(theme = {}) {
130 | this.theme = theme
131 |
132 | const renderers = Object.keys(this.theme)
133 | renderers.forEach(renderer => extendRenderer(this, renderer))
134 | }
135 |
136 | update() {
137 | return Promise.all([this.loadMeta(), this.loadUserInfo()])
138 | .then(() => Promise.all([
139 | this.loadComments().then(() => this.loadCommentReactions()),
140 | this.loadReactions(),
141 | ]))
142 | .catch(e => this.state.error = e)
143 | }
144 |
145 | markdown(text) {
146 | return http.post('/markdown', {
147 | text,
148 | mode: 'gfm',
149 | })
150 | }
151 |
152 | createIssue() {
153 | const { id, owner, repo, title, link, desc, labels } = this
154 |
155 | return http.post(`/repos/${owner}/${repo}/issues`, {
156 | title,
157 | labels: labels.concat(['gitment', id]),
158 | body: `${link}\n\n${desc}`,
159 | })
160 | .then((meta) => {
161 | this.state.meta = meta
162 | return meta
163 | })
164 | }
165 |
166 | getIssue() {
167 | if (this.state.meta.id) return Promise.resolve(this.state.meta)
168 |
169 | return this.loadMeta()
170 | }
171 |
172 | post(body) {
173 | return this.getIssue()
174 | .then(issue => http.post(issue.comments_url, { body }, ''))
175 | .then(data => {
176 | this.state.meta.comments++
177 | const pageCount = Math.ceil(this.state.meta.comments / this.perPage)
178 | if (this.state.currentPage === pageCount) {
179 | this.state.comments.push(data)
180 | }
181 | return data
182 | })
183 | }
184 |
185 | loadMeta() {
186 | const { id, owner, repo } = this
187 | return http.get(`/repos/${owner}/${repo}/issues`, {
188 | creator: owner,
189 | labels: id,
190 | })
191 | .then(issues => {
192 | if (!issues.length) return Promise.reject(NOT_INITIALIZED_ERROR)
193 | this.state.meta = issues[0]
194 | return issues[0]
195 | })
196 | }
197 |
198 | loadComments(page = this.state.currentPage) {
199 | return this.getIssue()
200 | .then(issue => http.get(issue.comments_url, { page, per_page: this.perPage }, ''))
201 | .then((comments) => {
202 | this.state.comments = comments
203 | return comments
204 | })
205 | }
206 |
207 | loadUserInfo() {
208 | if (!this.accessToken) {
209 | this.logout()
210 | return Promise.resolve({})
211 | }
212 |
213 | return http.get('/user')
214 | .then((user) => {
215 | this.state.user = user
216 | localStorage.setItem(LS_USER_KEY, JSON.stringify(user))
217 | return user
218 | })
219 | }
220 |
221 | loadReactions() {
222 | if (!this.accessToken) {
223 | this.state.reactions = []
224 | return Promise.resolve([])
225 | }
226 |
227 | return this.getIssue()
228 | .then((issue) => {
229 | if (!issue.reactions.total_count) return []
230 | return http.get(issue.reactions.url, {}, '')
231 | })
232 | .then((reactions) => {
233 | this.state.reactions = reactions
234 | return reactions
235 | })
236 | }
237 |
238 | loadCommentReactions() {
239 | if (!this.accessToken) {
240 | this.state.commentReactions = {}
241 | return Promise.resolve([])
242 | }
243 |
244 | const comments = this.state.comments
245 | const comentReactions = {}
246 |
247 | return Promise.all(comments.map((comment) => {
248 | if (!comment.reactions.total_count) return []
249 |
250 | const { owner, repo } = this
251 | return http.get(`/repos/${owner}/${repo}/issues/comments/${comment.id}/reactions`, {})
252 | }))
253 | .then(reactionsArray => {
254 | comments.forEach((comment, index) => {
255 | comentReactions[comment.id] = reactionsArray[index]
256 | })
257 | this.state.commentReactions = comentReactions
258 |
259 | return comentReactions
260 | })
261 | }
262 |
263 | login() {
264 | window.location.href = this.loginLink
265 | }
266 |
267 | logout() {
268 | localStorage.removeItem(LS_ACCESS_TOKEN_KEY)
269 | localStorage.removeItem(LS_USER_KEY)
270 | this.state.user = {}
271 | }
272 |
273 | goto(page) {
274 | this.state.currentPage = page
275 | this.state.comments = undefined
276 | return this.loadComments(page)
277 | }
278 |
279 | like() {
280 | if (!this.accessToken) {
281 | alert('Login to Like')
282 | return Promise.reject()
283 | }
284 |
285 | const { owner, repo } = this
286 |
287 | return http.post(`/repos/${owner}/${repo}/issues/${this.state.meta.number}/reactions`, {
288 | content: 'heart',
289 | })
290 | .then(reaction => {
291 | this.state.reactions.push(reaction)
292 | this.state.meta.reactions.heart++
293 | })
294 | }
295 |
296 | unlike() {
297 | if (!this.accessToken) return Promise.reject()
298 |
299 |
300 | const { user, reactions } = this.state
301 | const index = reactions.findIndex(reaction => reaction.user.login === user.login)
302 | return http.delete(`/reactions/${reactions[index].id}`)
303 | .then(() => {
304 | reactions.splice(index, 1)
305 | this.state.meta.reactions.heart--
306 | })
307 | }
308 |
309 | likeAComment(commentId) {
310 | if (!this.accessToken) {
311 | alert('Login to Like')
312 | return Promise.reject()
313 | }
314 |
315 | const { owner, repo } = this
316 | const comment = this.state.comments.find(comment => comment.id === commentId)
317 |
318 | return http.post(`/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`, {
319 | content: 'heart',
320 | })
321 | .then(reaction => {
322 | this.state.commentReactions[commentId].push(reaction)
323 | comment.reactions.heart++
324 | })
325 | }
326 |
327 | unlikeAComment(commentId) {
328 | if (!this.accessToken) return Promise.reject()
329 |
330 | const reactions = this.state.commentReactions[commentId]
331 | const comment = this.state.comments.find(comment => comment.id === commentId)
332 | const { user } = this.state
333 | const index = reactions.findIndex(reaction => reaction.user.login === user.login)
334 |
335 | return http.delete(`/reactions/${reactions[index].id}`)
336 | .then(() => {
337 | reactions.splice(index, 1)
338 | comment.reactions.heart--
339 | })
340 | }
341 | }
342 |
343 | module.exports = Gitment
344 |
--------------------------------------------------------------------------------
/src/icons.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Modified from https://github.com/evil-icons/evil-icons
3 | */
4 |
5 | export const close = ''
6 | export const github = ''
7 | export const heart = ''
8 | export const spinner = ''
9 |
--------------------------------------------------------------------------------
/src/test.js:
--------------------------------------------------------------------------------
1 | var Gitment = Gitment || require('./gitment')
2 |
3 | const config = window.config
4 |
5 | if (!config) {
6 | throw new Error('You need your own config to run this test.')
7 | }
8 |
9 | const gitment = new Gitment(config)
10 |
11 | gitment.render('container')
12 |
13 | window.gitment = gitment
14 |
15 | try {
16 | window.http = require('./utils').http
17 | } catch (e) {}
18 |
--------------------------------------------------------------------------------
/src/theme/default.js:
--------------------------------------------------------------------------------
1 | import { github as githubIcon, heart as heartIcon, spinner as spinnerIcon } from '../icons'
2 | import { NOT_INITIALIZED_ERROR } from '../constants'
3 |
4 | function renderHeader({ meta, user, reactions }, instance) {
5 | const container = document.createElement('div')
6 | container.lang = "en-US"
7 | container.className = 'gitment-container gitment-header-container'
8 |
9 | const likeButton = document.createElement('span')
10 | const likedReaction = reactions.find(reaction => (
11 | reaction.content === 'heart' && reaction.user.login === user.login
12 | ))
13 | likeButton.className = 'gitment-header-like-btn'
14 | likeButton.innerHTML = `
15 | ${heartIcon}
16 | ${ likedReaction
17 | ? 'Unlike'
18 | : 'Like'
19 | }
20 | ${ meta.reactions && meta.reactions.heart
21 | ? ` • ${meta.reactions.heart} Liked`
22 | : ''
23 | }
24 | `
25 |
26 | if (likedReaction) {
27 | likeButton.classList.add('liked')
28 | likeButton.onclick = () => instance.unlike()
29 | } else {
30 | likeButton.classList.remove('liked')
31 | likeButton.onclick = () => instance.like()
32 | }
33 | container.appendChild(likeButton)
34 |
35 | const commentsCount = document.createElement('span')
36 | commentsCount.innerHTML = `
37 | ${ meta.comments
38 | ? ` • ${meta.comments} Comments`
39 | : ''
40 | }
41 | `
42 | container.appendChild(commentsCount)
43 |
44 | const issueLink = document.createElement('a')
45 | issueLink.className = 'gitment-header-issue-link'
46 | issueLink.href = meta.html_url
47 | issueLink.target = '_blank'
48 | issueLink.innerText = 'Issue Page'
49 | container.appendChild(issueLink)
50 |
51 | return container
52 | }
53 |
54 | function renderComments({ meta, comments, commentReactions, currentPage, user, error }, instance) {
55 | const container = document.createElement('div')
56 | container.lang = "en-US"
57 | container.className = 'gitment-container gitment-comments-container'
58 |
59 | if (error) {
60 | const errorBlock = document.createElement('div')
61 | errorBlock.className = 'gitment-comments-error'
62 |
63 | if (error === NOT_INITIALIZED_ERROR
64 | && user.login
65 | && user.login.toLowerCase() === instance.owner.toLowerCase()) {
66 | const initHint = document.createElement('div')
67 | const initButton = document.createElement('button')
68 | initButton.className = 'gitment-comments-init-btn'
69 | initButton.onclick = () => {
70 | initButton.setAttribute('disabled', true)
71 | instance.init()
72 | .catch(e => {
73 | initButton.removeAttribute('disabled')
74 | alert(e)
75 | })
76 | }
77 | initButton.innerText = 'Initialize Comments'
78 | initHint.appendChild(initButton)
79 | errorBlock.appendChild(initHint)
80 | } else {
81 | errorBlock.innerText = error
82 | }
83 | container.appendChild(errorBlock)
84 | return container
85 | } else if (comments === undefined) {
86 | const loading = document.createElement('div')
87 | loading.innerText = 'Loading comments...'
88 | loading.className = 'gitment-comments-loading'
89 | container.appendChild(loading)
90 | return container
91 | } else if (!comments.length) {
92 | const emptyBlock = document.createElement('div')
93 | emptyBlock.className = 'gitment-comments-empty'
94 | emptyBlock.innerText = 'No Comment Yet'
95 | container.appendChild(emptyBlock)
96 | return container
97 | }
98 |
99 | const commentsList = document.createElement('ul')
100 | commentsList.className = 'gitment-comments-list'
101 |
102 | comments.forEach(comment => {
103 | const createDate = new Date(comment.created_at)
104 | const updateDate = new Date(comment.updated_at)
105 | const commentItem = document.createElement('li')
106 | commentItem.className = 'gitment-comment'
107 | commentItem.innerHTML = `
108 |
111 |
112 |
124 |
${comment.body_html}
125 |
126 | `
127 | const likeButton = commentItem.querySelector('.gitment-comment-like-btn')
128 | const likedReaction = commentReactions[comment.id]
129 | && commentReactions[comment.id].find(reaction => (
130 | reaction.content === 'heart' && reaction.user.login === user.login
131 | ))
132 | if (likedReaction) {
133 | likeButton.classList.add('liked')
134 | likeButton.onclick = () => instance.unlikeAComment(comment.id)
135 | } else {
136 | likeButton.classList.remove('liked')
137 | likeButton.onclick = () => instance.likeAComment(comment.id)
138 | }
139 |
140 | // dirty
141 | // use a blank image to trigger height calculating when element rendered
142 | const imgTrigger = document.createElement('img')
143 | const markdownBody = commentItem.querySelector('.gitment-comment-body')
144 | imgTrigger.className = 'gitment-hidden'
145 | imgTrigger.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
146 | imgTrigger.onload = () => {
147 | if (markdownBody.clientHeight > instance.maxCommentHeight) {
148 | markdownBody.classList.add('gitment-comment-body-folded')
149 | markdownBody.style.maxHeight = instance.maxCommentHeight + 'px'
150 | markdownBody.title = 'Click to Expand'
151 | markdownBody.onclick = () => {
152 | markdownBody.classList.remove('gitment-comment-body-folded')
153 | markdownBody.style.maxHeight = ''
154 | markdownBody.title = ''
155 | markdownBody.onclick = null
156 | }
157 | }
158 | }
159 | commentItem.appendChild(imgTrigger)
160 |
161 | commentsList.appendChild(commentItem)
162 | })
163 |
164 | container.appendChild(commentsList)
165 |
166 | if (meta) {
167 | const pageCount = Math.ceil(meta.comments / instance.perPage)
168 | if (pageCount > 1) {
169 | const pagination = document.createElement('ul')
170 | pagination.className = 'gitment-comments-pagination'
171 |
172 | if (currentPage > 1) {
173 | const previousButton = document.createElement('li')
174 | previousButton.className = 'gitment-comments-page-item'
175 | previousButton.innerText = 'Previous'
176 | previousButton.onclick = () => instance.goto(currentPage - 1)
177 | pagination.appendChild(previousButton)
178 | }
179 |
180 | for (let i = 1; i <= pageCount; i++) {
181 | const pageItem = document.createElement('li')
182 | pageItem.className = 'gitment-comments-page-item'
183 | pageItem.innerText = i
184 | pageItem.onclick = () => instance.goto(i)
185 | if (currentPage === i) pageItem.classList.add('gitment-selected')
186 | pagination.appendChild(pageItem)
187 | }
188 |
189 | if (currentPage < pageCount) {
190 | const nextButton = document.createElement('li')
191 | nextButton.className = 'gitment-comments-page-item'
192 | nextButton.innerText = 'Next'
193 | nextButton.onclick = () => instance.goto(currentPage + 1)
194 | pagination.appendChild(nextButton)
195 | }
196 |
197 | container.appendChild(pagination)
198 | }
199 | }
200 |
201 | return container
202 | }
203 |
204 | function renderEditor({ user, error }, instance) {
205 | const container = document.createElement('div')
206 | container.lang = "en-US"
207 | container.className = 'gitment-container gitment-editor-container'
208 |
209 | const shouldDisable = user.login && !error ? '' : 'disabled'
210 | const disabledTip = user.login ? '' : 'Login to Comment'
211 | container.innerHTML = `
212 | ${ user.login
213 | ? `
214 |
215 | `
216 | : user.isLoggingIn
217 | ? `${spinnerIcon}
`
218 | : `
219 | ${githubIcon}
220 | `
221 | }
222 |
223 |
224 |
238 |
239 |
240 |
241 |
242 |
245 |
246 |
247 |
253 | `
254 | if (user.login) {
255 | container.querySelector('.gitment-editor-logout-link').onclick = () => instance.logout()
256 | }
257 |
258 | const writeField = container.querySelector('.gitment-editor-write-field')
259 | const previewField = container.querySelector('.gitment-editor-preview-field')
260 |
261 | const textarea = writeField.querySelector('textarea')
262 | textarea.oninput = () => {
263 | textarea.style.height = 'auto'
264 | const style = window.getComputedStyle(textarea, null)
265 | const height = parseInt(style.height, 10)
266 | const clientHeight = textarea.clientHeight
267 | const scrollHeight = textarea.scrollHeight
268 | if (clientHeight < scrollHeight) {
269 | textarea.style.height = (height + scrollHeight - clientHeight) + 'px'
270 | }
271 | }
272 |
273 | const [writeTab, previewTab] = container.querySelectorAll('.gitment-editor-tab')
274 | writeTab.onclick = () => {
275 | writeTab.classList.add('gitment-selected')
276 | previewTab.classList.remove('gitment-selected')
277 | writeField.classList.remove('gitment-hidden')
278 | previewField.classList.add('gitment-hidden')
279 |
280 | textarea.focus()
281 | }
282 | previewTab.onclick = () => {
283 | previewTab.classList.add('gitment-selected')
284 | writeTab.classList.remove('gitment-selected')
285 | previewField.classList.remove('gitment-hidden')
286 | writeField.classList.add('gitment-hidden')
287 |
288 | const preview = previewField.querySelector('.gitment-editor-preview')
289 | const content = textarea.value.trim()
290 | if (!content) {
291 | preview.innerText = 'Nothing to preview'
292 | return
293 | }
294 |
295 | preview.innerText = 'Loading preview...'
296 | instance.markdown(content)
297 | .then(html => preview.innerHTML = html)
298 | }
299 |
300 | const submitButton = container.querySelector('.gitment-editor-submit')
301 | submitButton.onclick = () => {
302 | submitButton.innerText = 'Submitting...'
303 | submitButton.setAttribute('disabled', true)
304 | instance.post(textarea.value.trim())
305 | .then(data => {
306 | textarea.value = ''
307 | textarea.style.height = 'auto'
308 | submitButton.removeAttribute('disabled')
309 | submitButton.innerText = 'Comment'
310 | })
311 | .catch(e => {
312 | alert(e)
313 | submitButton.removeAttribute('disabled')
314 | submitButton.innerText = 'Comment'
315 | })
316 | }
317 |
318 | return container
319 | }
320 |
321 | function renderFooter() {
322 | const container = document.createElement('div')
323 | container.lang = "en-US"
324 | container.className = 'gitment-container gitment-footer-container'
325 | container.innerHTML = `
326 | Powered by
327 |
330 | `
331 | return container
332 | }
333 |
334 | function render(state, instance) {
335 | const container = document.createElement('div')
336 | container.lang = "en-US"
337 | container.className = 'gitment-container gitment-root-container'
338 | container.appendChild(instance.renderHeader(state, instance))
339 | container.appendChild(instance.renderComments(state, instance))
340 | container.appendChild(instance.renderEditor(state, instance))
341 | container.appendChild(instance.renderFooter(state, instance))
342 | return container
343 | }
344 |
345 | export default { render, renderHeader, renderComments, renderEditor, renderFooter }
346 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { LS_ACCESS_TOKEN_KEY } from './constants'
2 |
3 | export const isString = s => toString.call(s) === '[object String]'
4 |
5 | export function getTargetContainer(container) {
6 | let targetContainer
7 | if (container instanceof Element) {
8 | targetContainer = container
9 | } else if (isString(container)) {
10 | targetContainer = document.getElementById(container)
11 | } else {
12 | targetContainer = document.createElement('div')
13 | }
14 |
15 | return targetContainer
16 | }
17 |
18 | export const Query = {
19 | parse(search = window.location.search) {
20 | if (!search) return {}
21 | const queryString = search[0] === '?' ? search.substring(1) : search
22 | const query = {}
23 | queryString.split('&')
24 | .forEach(queryStr => {
25 | const [key, value] = queryStr.split('=')
26 | if (key) query[key] = value
27 | })
28 |
29 | return query
30 | },
31 | stringify(query, prefix = '?') {
32 | const queryString = Object.keys(query)
33 | .map(key => `${key}=${encodeURIComponent(query[key] || '')}`)
34 | .join('&')
35 | return queryString ? prefix + queryString : ''
36 | },
37 | }
38 |
39 | function ajaxFactory(method) {
40 | return function(apiPath, data = {}, base = 'https://api.github.com') {
41 | const req = new XMLHttpRequest()
42 | const token = localStorage.getItem(LS_ACCESS_TOKEN_KEY)
43 |
44 | let url = `${base}${apiPath}`
45 | let body = null
46 | if (method === 'GET' || method === 'DELETE') {
47 | url += Query.stringify(data)
48 | }
49 |
50 | const p = new Promise((resolve, reject) => {
51 | req.addEventListener('load', () => {
52 | const contentType = req.getResponseHeader('content-type')
53 | const res = req.responseText
54 | if (!/json/.test(contentType)) {
55 | resolve(res)
56 | return
57 | }
58 | const data = req.responseText ? JSON.parse(res) : {}
59 | if (data.message) {
60 | reject(new Error(data.message))
61 | } else {
62 | resolve(data)
63 | }
64 | })
65 | req.addEventListener('error', error => reject(error))
66 | })
67 | req.open(method, url, true)
68 |
69 | req.setRequestHeader('Accept', 'application/vnd.github.squirrel-girl-preview, application/vnd.github.html+json')
70 | if (token) {
71 | req.setRequestHeader('Authorization', `token ${token}`)
72 | }
73 | if (method !== 'GET' && method !== 'DELETE') {
74 | body = JSON.stringify(data)
75 | req.setRequestHeader('Content-Type', 'application/json')
76 | }
77 |
78 | req.send(body)
79 | return p
80 | }
81 | }
82 |
83 | export const http = {
84 | get: ajaxFactory('GET'),
85 | post: ajaxFactory('POST'),
86 | delete: ajaxFactory('DELETE'),
87 | put: ajaxFactory('PUT'),
88 | }
89 |
--------------------------------------------------------------------------------
/style/default.css:
--------------------------------------------------------------------------------
1 | .gitment-container {
2 | font-family: sans-serif;
3 | font-size: 14px;
4 | line-height: 1.5;
5 | color: #333;
6 | word-wrap: break-word;
7 | }
8 |
9 | .gitment-container * {
10 | box-sizing: border-box;
11 | }
12 |
13 | .gitment-container *:disabled {
14 | cursor: not-allowed;
15 | }
16 |
17 | .gitment-container a,
18 | .gitment-container a:visited {
19 | cursor: pointer;
20 | text-decoration: none;
21 | }
22 |
23 | .gitment-container a:hover {
24 | text-decoration: underline;
25 | }
26 |
27 | .gitment-container .gitment-hidden {
28 | display: none;
29 | }
30 |
31 | .gitment-container .gitment-spinner-icon {
32 | fill: #333;
33 |
34 | -webkit-animation: gitment-spin 1s steps(12) infinite;
35 | animation: gitment-spin 1s steps(12) infinite;
36 | }
37 |
38 | @-webkit-keyframes gitment-spin {
39 | 100% {
40 | -webkit-transform: rotate(360deg);
41 | transform: rotate(360deg)
42 | }
43 | }
44 |
45 | @keyframes gitment-spin {
46 | 100% {
47 | -webkit-transform: rotate(360deg);
48 | transform: rotate(360deg)
49 | }
50 | }
51 |
52 | .gitment-root-container {
53 | margin: 19px 0;
54 | }
55 |
56 | .gitment-header-container {
57 | margin: 19px 0;
58 | }
59 |
60 | .gitment-header-like-btn,
61 | .gitment-comment-like-btn {
62 | cursor: pointer;
63 | }
64 |
65 | .gitment-comment-like-btn {
66 | float: right;
67 | }
68 |
69 | .gitment-comment-like-btn.liked {
70 | color: #F44336;
71 | }
72 |
73 | .gitment-header-like-btn svg {
74 | vertical-align: middle;
75 | height: 30px;
76 | }
77 |
78 | .gitment-comment-like-btn svg {
79 | vertical-align: middle;
80 | height: 20px;
81 | }
82 |
83 | .gitment-header-like-btn.liked svg,
84 | .gitment-comment-like-btn.liked svg {
85 | fill: #F44336;
86 | }
87 |
88 | a.gitment-header-issue-link,
89 | a.gitment-header-issue-link:visited {
90 | float: right;
91 | line-height: 30px;
92 | color: #666;
93 | }
94 |
95 | a.gitment-header-issue-link:hover {
96 | color: #666;
97 | }
98 |
99 | .gitment-comments-loading,
100 | .gitment-comments-error,
101 | .gitment-comments-empty {
102 | text-align: center;
103 | margin: 50px 0;
104 | }
105 |
106 | .gitment-comments-list {
107 | list-style: none;
108 | padding-left: 0;
109 | margin: 0 0 38px;
110 | }
111 |
112 | .gitment-comment,
113 | .gitment-editor-container {
114 | position: relative;
115 | min-height: 60px;
116 | padding-left: 60px;
117 | margin: 19px 0;
118 | }
119 |
120 | .gitment-comment-avatar,
121 | .gitment-editor-avatar {
122 | float: left;
123 | margin-left: -60px;
124 | }
125 |
126 | .gitment-comment-avatar,
127 | .gitment-comment-avatar-img,
128 | .gitment-comment-avatar,
129 | .gitment-editor-avatar-img,
130 | .gitment-editor-avatar svg {
131 | width: 44px;
132 | height: 44px;
133 | border-radius: 3px;
134 | }
135 |
136 | .gitment-editor-avatar .gitment-github-icon {
137 | fill: #fff;
138 | background-color: #333;
139 | }
140 |
141 | .gitment-comment-main,
142 | .gitment-editor-main {
143 | position: relative;
144 | border: 1px solid #CFD8DC;
145 | border-radius: 0;
146 | }
147 |
148 | .gitment-editor-main::before,
149 | .gitment-editor-main::after,
150 | .gitment-comment-main::before,
151 | .gitment-comment-main::after {
152 | position: absolute;
153 | top: 11px;
154 | left: -16px;
155 | display: block;
156 | width: 0;
157 | height: 0;
158 | pointer-events: none;
159 | content: "";
160 | border-color: transparent;
161 | border-style: solid solid outset;
162 | }
163 |
164 | .gitment-editor-main::before,
165 | .gitment-comment-main::before {
166 | border-width: 8px;
167 | border-right-color: #CFD8DC;
168 | }
169 |
170 | .gitment-editor-main::after,
171 | .gitment-comment-main::after {
172 | margin-top: 1px;
173 | margin-left: 2px;
174 | border-width: 7px;
175 | border-right-color: #fff;
176 | }
177 |
178 | .gitment-comment-header {
179 | margin: 12px 15px;
180 | color: #666;
181 | background-color: #fff;
182 | border-radius: 3px;
183 | }
184 |
185 | .gitment-editor-header {
186 | padding: 0;
187 | margin: 0;
188 | border-bottom: 1px solid #CFD8DC;
189 | }
190 |
191 | a.gitment-comment-name,
192 | a.gitment-comment-name:visited {
193 | font-weight: 600;
194 | color: #666;
195 | }
196 |
197 | .gitment-editor-tabs {
198 | margin-bottom: -1px;
199 | margin-left: -1px;
200 | }
201 |
202 | .gitment-editor-tab {
203 | display: inline-block;
204 | padding: 11px 12px;
205 | font-size: 14px;
206 | line-height: 20px;
207 | color: #666;
208 | text-decoration: none;
209 | background-color: transparent;
210 | border-width: 0 1px;
211 | border-style: solid;
212 | border-color: transparent;
213 | border-radius: 0;
214 |
215 | white-space: nowrap;
216 | cursor: pointer;
217 | user-select: none;
218 |
219 | outline: none;
220 | }
221 |
222 | .gitment-editor-tab.gitment-selected {
223 | color: #333;
224 | background-color: #fff;
225 | border-color: #CFD8DC;
226 | }
227 |
228 | .gitment-editor-login {
229 | float: right;
230 | margin-top: -30px;
231 | margin-right: 15px;
232 | }
233 |
234 | a.gitment-footer-project-link,
235 | a.gitment-footer-project-link:visited,
236 | a.gitment-editor-login-link,
237 | a.gitment-editor-login-link:visited {
238 | color: #2196F3;
239 | }
240 |
241 | a.gitment-editor-logout-link,
242 | a.gitment-editor-logout-link:visited {
243 | color: #666;
244 | }
245 |
246 | a.gitment-editor-logout-link:hover {
247 | color: #2196F3;
248 | text-decoration: none;
249 | }
250 |
251 | .gitment-comment-body {
252 | position: relative;
253 | margin: 12px 15px;
254 | overflow: hidden;
255 | border-radius: 3px;
256 | }
257 |
258 | .gitment-comment-body-folded {
259 | cursor: pointer;
260 | }
261 |
262 | .gitment-comment-body-folded::before {
263 | display: block !important;
264 | content: "";
265 | position: absolute;
266 | width: 100%;
267 | left: 0;
268 | top: 0;
269 | bottom: 50px;
270 | pointer-events: none;
271 | background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .9));
272 | background: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, .9));
273 | }
274 |
275 | .gitment-comment-body-folded::after {
276 | display: block !important;
277 | content: "Click to Expand" !important;
278 | text-align: center;
279 | color: #666;
280 | position: absolute;
281 | width: 100%;
282 | height: 50px;
283 | line-height: 50px;
284 | left: 0;
285 | bottom: 0;
286 | pointer-events: none;
287 | background: rgba(255, 255, 255, .9);
288 | }
289 |
290 | .gitment-editor-body {
291 | margin: 0;
292 | }
293 |
294 | .gitment-comment-body > *:first-child,
295 | .gitment-editor-preview > *:first-child {
296 | margin-top: 0 !important;
297 | }
298 |
299 | .gitment-comment-body > *:last-child,
300 | .gitment-editor-preview > *:last-child {
301 | margin-bottom: 0 !important;
302 | }
303 |
304 | .gitment-editor-body textarea {
305 | display: block;
306 | width: 100%;
307 | min-height: 150px;
308 | max-height: 500px;
309 | padding: 16px;
310 | resize: vertical;
311 |
312 | max-width: 100%;
313 | margin: 0;
314 | font-size: 14px;
315 | line-height: 1.6;
316 |
317 | background-color: #fff;
318 |
319 | color: #333;
320 | vertical-align: middle;
321 | border: none;
322 | border-radius: 0;
323 | outline: none;
324 | box-shadow: none;
325 |
326 | overflow: visible;
327 | }
328 |
329 | .gitment-editor-body textarea:focus {
330 | background-color: #fff;
331 | }
332 |
333 | .gitment-editor-preview {
334 | min-height: 150px;
335 |
336 | padding: 16px;
337 | background-color: transparent;
338 |
339 | width: 100%;
340 | font-size: 14px;
341 |
342 | line-height: 1.5;
343 | word-wrap: break-word;
344 | }
345 |
346 | .gitment-editor-footer {
347 | padding: 0;
348 | margin-top: 10px;
349 | }
350 |
351 | .gitment-editor-footer::after {
352 | display: table;
353 | clear: both;
354 | content: "";
355 | }
356 |
357 | a.gitment-editor-footer-tip {
358 | display: inline-block;
359 | padding-top: 10px;
360 | font-size: 12px;
361 | color: #666;
362 | }
363 |
364 | a.gitment-editor-footer-tip:hover {
365 | color: #2196F3;
366 | text-decoration: none;
367 | }
368 |
369 | .gitment-comments-pagination {
370 | list-style: none;
371 | text-align: right;
372 | border-radius: 0;
373 | margin: -19px 0 19px 0;
374 | }
375 |
376 | .gitment-comments-page-item {
377 | display: inline-block;
378 | cursor: pointer;
379 | border: 1px solid #CFD8DC;
380 | margin-left: -1px;
381 | padding: .25rem .5rem;
382 | }
383 |
384 | .gitment-comments-page-item:hover {
385 | background-color: #f5f5f5;
386 | }
387 |
388 | .gitment-comments-page-item.gitment-selected {
389 | background-color: #f5f5f5;
390 | }
391 |
392 | .gitment-editor-submit,
393 | .gitment-comments-init-btn {
394 | color: #fff;
395 | background-color: #00BCD4;
396 |
397 | position: relative;
398 | display: inline-block;
399 | padding: 7px 13px;
400 | font-size: 14px;
401 | font-weight: 600;
402 | line-height: 20px;
403 | white-space: nowrap;
404 | vertical-align: middle;
405 | cursor: pointer;
406 | -webkit-user-select: none;
407 | -moz-user-select: none;
408 | -ms-user-select: none;
409 | user-select: none;
410 | background-size: 110% 110%;
411 | border: none;
412 | -webkit-appearance: none;
413 | -moz-appearance: none;
414 | appearance: none;
415 | }
416 |
417 | .gitment-editor-submit:hover,
418 | .gitment-comments-init-btn:hover {
419 | background-color: #00ACC1;
420 | }
421 |
422 | .gitment-comments-init-btn:disabled,
423 | .gitment-editor-submit:disabled {
424 | color: rgba(255,255,255,0.75);
425 | background-color: #4DD0E1;
426 | box-shadow: none;
427 | }
428 |
429 | .gitment-editor-submit {
430 | float: right;
431 | }
432 |
433 | .gitment-footer-container {
434 | margin-top: 30px;
435 | margin-bottom: 20px;
436 | text-align: right;
437 | font-size: 12px;
438 | }
439 |
440 | /*
441 | * Markdown CSS
442 | * Copied from https://github.com/sindresorhus/github-markdown-css
443 | */
444 | .gitment-markdown {
445 | -ms-text-size-adjust: 100%;
446 | -webkit-text-size-adjust: 100%;
447 | line-height: 1.5;
448 | color: #333;
449 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
450 | font-size: 16px;
451 | line-height: 1.5;
452 | word-wrap: break-word;
453 | }
454 |
455 | .gitment-markdown .pl-c {
456 | color: #969896;
457 | }
458 |
459 | .gitment-markdown .pl-c1,
460 | .gitment-markdown .pl-s .pl-v {
461 | color: #0086b3;
462 | }
463 |
464 | .gitment-markdown .pl-e,
465 | .gitment-markdown .pl-en {
466 | color: #795da3;
467 | }
468 |
469 | .gitment-markdown .pl-smi,
470 | .gitment-markdown .pl-s .pl-s1 {
471 | color: #333;
472 | }
473 |
474 | .gitment-markdown .pl-ent {
475 | color: #63a35c;
476 | }
477 |
478 | .gitment-markdown .pl-k {
479 | color: #a71d5d;
480 | }
481 |
482 | .gitment-markdown .pl-s,
483 | .gitment-markdown .pl-pds,
484 | .gitment-markdown .pl-s .pl-pse .pl-s1,
485 | .gitment-markdown .pl-sr,
486 | .gitment-markdown .pl-sr .pl-cce,
487 | .gitment-markdown .pl-sr .pl-sre,
488 | .gitment-markdown .pl-sr .pl-sra {
489 | color: #183691;
490 | }
491 |
492 | .gitment-markdown .pl-v,
493 | .gitment-markdown .pl-smw {
494 | color: #ed6a43;
495 | }
496 |
497 | .gitment-markdown .pl-bu {
498 | color: #b52a1d;
499 | }
500 |
501 | .gitment-markdown .pl-ii {
502 | color: #f8f8f8;
503 | background-color: #b52a1d;
504 | }
505 |
506 | .gitment-markdown .pl-c2 {
507 | color: #f8f8f8;
508 | background-color: #b52a1d;
509 | }
510 |
511 | .gitment-markdown .pl-c2::before {
512 | content: "^M";
513 | }
514 |
515 | .gitment-markdown .pl-sr .pl-cce {
516 | font-weight: bold;
517 | color: #63a35c;
518 | }
519 |
520 | .gitment-markdown .pl-ml {
521 | color: #693a17;
522 | }
523 |
524 | .gitment-markdown .pl-mh,
525 | .gitment-markdown .pl-mh .pl-en,
526 | .gitment-markdown .pl-ms {
527 | font-weight: bold;
528 | color: #1d3e81;
529 | }
530 |
531 | .gitment-markdown .pl-mq {
532 | color: #008080;
533 | }
534 |
535 | .gitment-markdown .pl-mi {
536 | font-style: italic;
537 | color: #333;
538 | }
539 |
540 | .gitment-markdown .pl-mb {
541 | font-weight: bold;
542 | color: #333;
543 | }
544 |
545 | .gitment-markdown .pl-md {
546 | color: #bd2c00;
547 | background-color: #ffecec;
548 | }
549 |
550 | .gitment-markdown .pl-mi1 {
551 | color: #55a532;
552 | background-color: #eaffea;
553 | }
554 |
555 | .gitment-markdown .pl-mc {
556 | color: #ef9700;
557 | background-color: #ffe3b4;
558 | }
559 |
560 | .gitment-markdown .pl-mi2 {
561 | color: #d8d8d8;
562 | background-color: #808080;
563 | }
564 |
565 | .gitment-markdown .pl-mdr {
566 | font-weight: bold;
567 | color: #795da3;
568 | }
569 |
570 | .gitment-markdown .pl-mo {
571 | color: #1d3e81;
572 | }
573 |
574 | .gitment-markdown .pl-ba {
575 | color: #595e62;
576 | }
577 |
578 | .gitment-markdown .pl-sg {
579 | color: #c0c0c0;
580 | }
581 |
582 | .gitment-markdown .pl-corl {
583 | text-decoration: underline;
584 | color: #183691;
585 | }
586 |
587 | .gitment-markdown .octicon {
588 | display: inline-block;
589 | vertical-align: text-top;
590 | fill: currentColor;
591 | }
592 |
593 | .gitment-markdown a {
594 | background-color: transparent;
595 | -webkit-text-decoration-skip: objects;
596 | }
597 |
598 | .gitment-markdown a:active,
599 | .gitment-markdown a:hover {
600 | outline-width: 0;
601 | }
602 |
603 | .gitment-markdown strong {
604 | font-weight: inherit;
605 | }
606 |
607 | .gitment-markdown strong {
608 | font-weight: bolder;
609 | }
610 |
611 | .gitment-markdown h1 {
612 | font-size: 2em;
613 | margin: 0.67em 0;
614 | }
615 |
616 | .gitment-markdown img {
617 | border-style: none;
618 | }
619 |
620 | .gitment-markdown svg:not(:root) {
621 | overflow: hidden;
622 | }
623 |
624 | .gitment-markdown code,
625 | .gitment-markdown kbd,
626 | .gitment-markdown pre {
627 | font-family: monospace, monospace;
628 | font-size: 1em;
629 | }
630 |
631 | .gitment-markdown hr {
632 | box-sizing: content-box;
633 | height: 0;
634 | overflow: visible;
635 | }
636 |
637 | .gitment-markdown input {
638 | font: inherit;
639 | margin: 0;
640 | }
641 |
642 | .gitment-markdown input {
643 | overflow: visible;
644 | }
645 |
646 | .gitment-markdown [type="checkbox"] {
647 | box-sizing: border-box;
648 | padding: 0;
649 | }
650 |
651 | .gitment-markdown * {
652 | box-sizing: border-box;
653 | }
654 |
655 | .gitment-markdown input {
656 | font-family: inherit;
657 | font-size: inherit;
658 | line-height: inherit;
659 | }
660 |
661 | .gitment-markdown a {
662 | color: #0366d6;
663 | text-decoration: none;
664 | }
665 |
666 | .gitment-markdown a:hover {
667 | text-decoration: underline;
668 | }
669 |
670 | .gitment-markdown strong {
671 | font-weight: 600;
672 | }
673 |
674 | .gitment-markdown hr {
675 | height: 0;
676 | margin: 15px 0;
677 | overflow: hidden;
678 | background: transparent;
679 | border: 0;
680 | border-bottom: 1px solid #dfe2e5;
681 | }
682 |
683 | .gitment-markdown hr::before {
684 | display: table;
685 | content: "";
686 | }
687 |
688 | .gitment-markdown hr::after {
689 | display: table;
690 | clear: both;
691 | content: "";
692 | }
693 |
694 | .gitment-markdown table {
695 | border-spacing: 0;
696 | border-collapse: collapse;
697 | }
698 |
699 | .gitment-markdown td,
700 | .gitment-markdown th {
701 | padding: 0;
702 | }
703 |
704 | .gitment-markdown h1,
705 | .gitment-markdown h2,
706 | .gitment-markdown h3,
707 | .gitment-markdown h4,
708 | .gitment-markdown h5,
709 | .gitment-markdown h6 {
710 | margin-top: 0;
711 | margin-bottom: 0;
712 | }
713 |
714 | .gitment-markdown h1 {
715 | font-size: 32px;
716 | font-weight: 600;
717 | }
718 |
719 | .gitment-markdown h2 {
720 | font-size: 24px;
721 | font-weight: 600;
722 | }
723 |
724 | .gitment-markdown h3 {
725 | font-size: 20px;
726 | font-weight: 600;
727 | }
728 |
729 | .gitment-markdown h4 {
730 | font-size: 16px;
731 | font-weight: 600;
732 | }
733 |
734 | .gitment-markdown h5 {
735 | font-size: 14px;
736 | font-weight: 600;
737 | }
738 |
739 | .gitment-markdown h6 {
740 | font-size: 12px;
741 | font-weight: 600;
742 | }
743 |
744 | .gitment-markdown p {
745 | margin-top: 0;
746 | margin-bottom: 10px;
747 | }
748 |
749 | .gitment-markdown blockquote {
750 | margin: 0;
751 | }
752 |
753 | .gitment-markdown ul,
754 | .gitment-markdown ol {
755 | padding-left: 0;
756 | margin-top: 0;
757 | margin-bottom: 0;
758 | }
759 |
760 | .gitment-markdown ol ol,
761 | .gitment-markdown ul ol {
762 | list-style-type: lower-roman;
763 | }
764 |
765 | .gitment-markdown ul ul ol,
766 | .gitment-markdown ul ol ol,
767 | .gitment-markdown ol ul ol,
768 | .gitment-markdown ol ol ol {
769 | list-style-type: lower-alpha;
770 | }
771 |
772 | .gitment-markdown dd {
773 | margin-left: 0;
774 | }
775 |
776 | .gitment-markdown code {
777 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
778 | font-size: 12px;
779 | }
780 |
781 | .gitment-markdown pre {
782 | margin-top: 0;
783 | margin-bottom: 0;
784 | font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
785 | }
786 |
787 | .gitment-markdown .octicon {
788 | vertical-align: text-bottom;
789 | }
790 |
791 | .gitment-markdown .pl-0 {
792 | padding-left: 0 !important;
793 | }
794 |
795 | .gitment-markdown .pl-1 {
796 | padding-left: 4px !important;
797 | }
798 |
799 | .gitment-markdown .pl-2 {
800 | padding-left: 8px !important;
801 | }
802 |
803 | .gitment-markdown .pl-3 {
804 | padding-left: 16px !important;
805 | }
806 |
807 | .gitment-markdown .pl-4 {
808 | padding-left: 24px !important;
809 | }
810 |
811 | .gitment-markdown .pl-5 {
812 | padding-left: 32px !important;
813 | }
814 |
815 | .gitment-markdown .pl-6 {
816 | padding-left: 40px !important;
817 | }
818 |
819 | .gitment-markdown::before {
820 | display: table;
821 | content: "";
822 | }
823 |
824 | .gitment-markdown::after {
825 | display: table;
826 | clear: both;
827 | content: "";
828 | }
829 |
830 | .gitment-markdown>*:first-child {
831 | margin-top: 0 !important;
832 | }
833 |
834 | .gitment-markdown>*:last-child {
835 | margin-bottom: 0 !important;
836 | }
837 |
838 | .gitment-markdown a:not([href]) {
839 | color: inherit;
840 | text-decoration: none;
841 | }
842 |
843 | .gitment-markdown .anchor {
844 | float: left;
845 | padding-right: 4px;
846 | margin-left: -20px;
847 | line-height: 1;
848 | }
849 |
850 | .gitment-markdown .anchor:focus {
851 | outline: none;
852 | }
853 |
854 | .gitment-markdown p,
855 | .gitment-markdown blockquote,
856 | .gitment-markdown ul,
857 | .gitment-markdown ol,
858 | .gitment-markdown dl,
859 | .gitment-markdown table,
860 | .gitment-markdown pre {
861 | margin-top: 0;
862 | margin-bottom: 16px;
863 | }
864 |
865 | .gitment-markdown hr {
866 | height: 0.25em;
867 | padding: 0;
868 | margin: 24px 0;
869 | background-color: #e1e4e8;
870 | border: 0;
871 | }
872 |
873 | .gitment-markdown blockquote {
874 | padding: 0 1em;
875 | color: #6a737d;
876 | border-left: 0.25em solid #dfe2e5;
877 | }
878 |
879 | .gitment-markdown blockquote>:first-child {
880 | margin-top: 0;
881 | }
882 |
883 | .gitment-markdown blockquote>:last-child {
884 | margin-bottom: 0;
885 | }
886 |
887 | .gitment-markdown kbd {
888 | display: inline-block;
889 | padding: 3px 5px;
890 | font-size: 11px;
891 | line-height: 10px;
892 | color: #444d56;
893 | vertical-align: middle;
894 | background-color: #fafbfc;
895 | border: solid 1px #c6cbd1;
896 | border-bottom-color: #959da5;
897 | border-radius: 0;
898 | box-shadow: inset 0 -1px 0 #959da5;
899 | }
900 |
901 | .gitment-markdown h1,
902 | .gitment-markdown h2,
903 | .gitment-markdown h3,
904 | .gitment-markdown h4,
905 | .gitment-markdown h5,
906 | .gitment-markdown h6 {
907 | margin-top: 24px;
908 | margin-bottom: 16px;
909 | font-weight: 600;
910 | line-height: 1.25;
911 | }
912 |
913 | .gitment-markdown h1 .octicon-link,
914 | .gitment-markdown h2 .octicon-link,
915 | .gitment-markdown h3 .octicon-link,
916 | .gitment-markdown h4 .octicon-link,
917 | .gitment-markdown h5 .octicon-link,
918 | .gitment-markdown h6 .octicon-link {
919 | color: #1b1f23;
920 | vertical-align: middle;
921 | visibility: hidden;
922 | }
923 |
924 | .gitment-markdown h1:hover .anchor,
925 | .gitment-markdown h2:hover .anchor,
926 | .gitment-markdown h3:hover .anchor,
927 | .gitment-markdown h4:hover .anchor,
928 | .gitment-markdown h5:hover .anchor,
929 | .gitment-markdown h6:hover .anchor {
930 | text-decoration: none;
931 | }
932 |
933 | .gitment-markdown h1:hover .anchor .octicon-link,
934 | .gitment-markdown h2:hover .anchor .octicon-link,
935 | .gitment-markdown h3:hover .anchor .octicon-link,
936 | .gitment-markdown h4:hover .anchor .octicon-link,
937 | .gitment-markdown h5:hover .anchor .octicon-link,
938 | .gitment-markdown h6:hover .anchor .octicon-link {
939 | visibility: visible;
940 | }
941 |
942 | .gitment-markdown h1 {
943 | padding-bottom: 0.3em;
944 | font-size: 2em;
945 | border-bottom: 1px solid #eaecef;
946 | }
947 |
948 | .gitment-markdown h2 {
949 | padding-bottom: 0.3em;
950 | font-size: 1.5em;
951 | border-bottom: 1px solid #eaecef;
952 | }
953 |
954 | .gitment-markdown h3 {
955 | font-size: 1.25em;
956 | }
957 |
958 | .gitment-markdown h4 {
959 | font-size: 1em;
960 | }
961 |
962 | .gitment-markdown h5 {
963 | font-size: 0.875em;
964 | }
965 |
966 | .gitment-markdown h6 {
967 | font-size: 0.85em;
968 | color: #6a737d;
969 | }
970 |
971 | .gitment-markdown ul,
972 | .gitment-markdown ol {
973 | padding-left: 2em;
974 | }
975 |
976 | .gitment-markdown ul ul,
977 | .gitment-markdown ul ol,
978 | .gitment-markdown ol ol,
979 | .gitment-markdown ol ul {
980 | margin-top: 0;
981 | margin-bottom: 0;
982 | }
983 |
984 | .gitment-markdown li>p {
985 | margin-top: 16px;
986 | }
987 |
988 | .gitment-markdown li+li {
989 | margin-top: 0.25em;
990 | }
991 |
992 | .gitment-markdown dl {
993 | padding: 0;
994 | }
995 |
996 | .gitment-markdown dl dt {
997 | padding: 0;
998 | margin-top: 16px;
999 | font-size: 1em;
1000 | font-style: italic;
1001 | font-weight: 600;
1002 | }
1003 |
1004 | .gitment-markdown dl dd {
1005 | padding: 0 16px;
1006 | margin-bottom: 16px;
1007 | }
1008 |
1009 | .gitment-markdown table {
1010 | display: block;
1011 | width: 100%;
1012 | overflow: auto;
1013 | }
1014 |
1015 | .gitment-markdown table th {
1016 | font-weight: 600;
1017 | }
1018 |
1019 | .gitment-markdown table th,
1020 | .gitment-markdown table td {
1021 | padding: 6px 13px;
1022 | border: 1px solid #dfe2e5;
1023 | }
1024 |
1025 | .gitment-markdown table tr {
1026 | background-color: #fff;
1027 | border-top: 1px solid #c6cbd1;
1028 | }
1029 |
1030 | .gitment-markdown table tr:nth-child(2n) {
1031 | background-color: #f5f5f5;
1032 | }
1033 |
1034 | .gitment-markdown img {
1035 | max-width: 100%;
1036 | box-sizing: content-box;
1037 | background-color: #fff;
1038 | }
1039 |
1040 | .gitment-markdown code {
1041 | padding: 0;
1042 | padding-top: 0.2em;
1043 | padding-bottom: 0.2em;
1044 | margin: 0;
1045 | font-size: 85%;
1046 | background-color: rgba(27,31,35,0.05);
1047 | border-radius: 0;
1048 | }
1049 |
1050 | .gitment-markdown code::before,
1051 | .gitment-markdown code::after {
1052 | letter-spacing: -0.2em;
1053 | content: "\00a0";
1054 | }
1055 |
1056 | .gitment-markdown pre {
1057 | word-wrap: normal;
1058 | }
1059 |
1060 | .gitment-markdown pre>code {
1061 | padding: 0;
1062 | margin: 0;
1063 | font-size: 100%;
1064 | word-break: normal;
1065 | white-space: pre;
1066 | background: transparent;
1067 | border: 0;
1068 | }
1069 |
1070 | .gitment-markdown .highlight {
1071 | margin-bottom: 16px;
1072 | }
1073 |
1074 | .gitment-markdown .highlight pre {
1075 | margin-bottom: 0;
1076 | word-break: normal;
1077 | }
1078 |
1079 | .gitment-markdown .highlight pre,
1080 | .gitment-markdown pre {
1081 | padding: 16px;
1082 | overflow: auto;
1083 | font-size: 85%;
1084 | line-height: 1.45;
1085 | background-color: #f5f5f5;
1086 | border-radius: 0;
1087 | }
1088 |
1089 | .gitment-markdown pre code {
1090 | display: inline;
1091 | max-width: auto;
1092 | padding: 0;
1093 | margin: 0;
1094 | overflow: visible;
1095 | line-height: inherit;
1096 | word-wrap: normal;
1097 | background-color: transparent;
1098 | border: 0;
1099 | }
1100 |
1101 | .gitment-markdown pre code::before,
1102 | .gitment-markdown pre code::after {
1103 | content: normal;
1104 | }
1105 |
1106 | .gitment-markdown .full-commit .btn-outline:not(:disabled):hover {
1107 | color: #005cc5;
1108 | border-color: #005cc5;
1109 | }
1110 |
1111 | .gitment-markdown kbd {
1112 | display: inline-block;
1113 | padding: 3px 5px;
1114 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
1115 | line-height: 10px;
1116 | color: #444d56;
1117 | vertical-align: middle;
1118 | background-color: #fcfcfc;
1119 | border: solid 1px #c6cbd1;
1120 | border-bottom-color: #959da5;
1121 | border-radius: 0;
1122 | box-shadow: inset 0 -1px 0 #959da5;
1123 | }
1124 |
1125 | .gitment-markdown :checked+.radio-label {
1126 | position: relative;
1127 | z-index: 1;
1128 | border-color: #0366d6;
1129 | }
1130 |
1131 | .gitment-markdown .task-list-item {
1132 | list-style-type: none;
1133 | }
1134 |
1135 | .gitment-markdown .task-list-item+.task-list-item {
1136 | margin-top: 3px;
1137 | }
1138 |
1139 | .gitment-markdown .task-list-item input {
1140 | margin: 0 0.2em 0.25em -1.6em;
1141 | vertical-align: middle;
1142 | }
1143 |
1144 | .gitment-markdown hr {
1145 | border-bottom-color: #eee;
1146 | }
1147 |
--------------------------------------------------------------------------------
/test/gitment.browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Gitment
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/gitment.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Gitment
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0 20px;
3 | margin: 0 auto;
4 | font-family: sans-serif;
5 | }
6 |
7 | @media (min-width: 780px) {
8 | body {
9 | max-width: 760px;
10 | }
11 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | context: path.join(__dirname, 'src'),
5 | entry: './gitment.js',
6 | devtool: 'source-map',
7 | output: {
8 | path: path.join(__dirname, 'dist'),
9 | filename: 'gitment.browser.js',
10 | libraryTarget: 'var',
11 | library: 'Gitment',
12 | },
13 | module: {
14 | loaders: [
15 | {
16 | test: /\.js$/,
17 | exclude: /^node_mocules/,
18 | loaders: ['babel-loader'],
19 | },
20 | ],
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | context: path.join(__dirname, 'src'),
5 | entry: './test.js',
6 | devtool: 'source-map',
7 | output: {
8 | path: path.join(__dirname, 'dist'),
9 | filename: 'test.js',
10 | publicPath: '/dist/',
11 | },
12 | module: {
13 | loaders: [
14 | {
15 | test: /\.js$/,
16 | exclude: /^node_mocules/,
17 | loaders: ['babel-loader'],
18 | },
19 | ],
20 | },
21 | devServer: {
22 | port: 3000,
23 | contentBase: './',
24 | },
25 | }
26 |
--------------------------------------------------------------------------------