##// END OF EJS Templates
front-end: use At.js for MentionsAutoComplete...
domruf -
r7387:2e7ffb75 default
parent child Browse files
Show More
@@ -1,50 +1,52 b''
1 1 syntax: glob
2 2 *.pyc
3 3 *.swp
4 4 *.sqlite
5 5 *.tox
6 6 *.egg-info
7 7 *.egg
8 8 *.mo
9 9 .eggs/
10 10 tarballcache/
11 11
12 12 syntax: regexp
13 13 ^rcextensions
14 14 ^build
15 15 ^dist/
16 16 ^docs/build/
17 17 ^docs/_build/
18 18 ^data$
19 19 ^sql_dumps/
20 20 ^\.settings$
21 21 ^\.project$
22 22 ^\.pydevproject$
23 23 ^\.coverage$
24 24 ^kallithea/front-end/node_modules$
25 25 ^kallithea/front-end/package-lock\.json$
26 26 ^kallithea/front-end/tmp$
27 27 ^kallithea/public/codemirror$
28 28 ^kallithea/public/css/select2-spinner\.gif$
29 29 ^kallithea/public/css/select2\.png$
30 30 ^kallithea/public/css/select2x2\.png$
31 31 ^kallithea/public/css/style\.css$
32 32 ^kallithea/public/css/style\.css\.map$
33 33 ^kallithea/public/js/bootstrap\.js$
34 34 ^kallithea/public/js/dataTables\.bootstrap\.js$
35 ^kallithea/public/js/jquery\.atwho\.min\.js$
36 ^kallithea/public/js/jquery\.caret\.min\.js$
35 37 ^kallithea/public/js/jquery\.dataTables\.js$
36 38 ^kallithea/public/js/jquery\.flot\.js$
37 39 ^kallithea/public/js/jquery\.flot\.selection\.js$
38 40 ^kallithea/public/js/jquery\.flot\.time\.js$
39 41 ^kallithea/public/js/jquery\.min\.js$
40 42 ^kallithea/public/js/select2\.js$
41 43 ^theme\.less$
42 44 ^kallithea\.db$
43 45 ^test\.db$
44 46 ^Kallithea\.egg-info$
45 47 ^my\.ini$
46 48 ^fabfile.py
47 49 ^\.idea$
48 50 ^\.cache$
49 51 ^\.pytest_cache$
50 52 /__pycache__$
@@ -1,311 +1,343 b''
1 1 Kallithea License
2 2 =================
3 3
4 4 Kallithea as a whole is copyrighted by various authors and is licensed under
5 5 the terms of the GNU General Public License, version 3 (GPLv3), which is a
6 6 license published by the Free Software Foundation,
7 7 Inc. [A copy of GPLv3](/COPYING) is included herein.
8 8
9 9 Some individual files have copyright notices and those who offer changes to
10 10 those files should update the copyright notices in those specific files if
11 11 they so chose.
12 12
13 13 However, the definitive list of copyright holders for this project is kept in
14 14 [the about page template](kallithea/templates/about.html) so that it is
15 15 displayed appropriately when Kallithea is installed. This is the most
16 16 important place to update copyright notices.
17 17
18 18 Third-Party Code Incorporated in Kallithea
19 19 ==========================================
20 20
21 21 Various third-party code under GPLv3-compatible licenses is included as part
22 22 of Kallithea.
23 23
24 24
25 25 Alembic
26 26 -------
27 27
28 28 Kallithea incorporates an [Alembic](http://alembic.zzzcomputing.com/en/latest/)
29 29 "migration environment" in `kallithea/alembic`, portions of which is:
30 30
31 31 Copyright © 2009-2016 by Michael Bayer.
32 32 Alembic is a trademark of Michael Bayer.
33 33
34 34 and licensed under the MIT-permissive license, which is
35 35 [included in this distribution](MIT-Permissive-License.txt).
36 36
37 37
38 38 Bootstrap
39 39 ---------
40 40
41 41 Kallithea uses the web framework called
42 42 [Bootstrap](http://getbootstrap.com/), which is:
43 43
44 44 Copyright © 2011-2016 Twitter, Inc.
45 45
46 46 and licensed under the MIT-permissive license, which is
47 47 [included in this distribution](MIT-Permissive-License.txt).
48 48
49 49 It is not distributed with Kallithea, but will be downloaded
50 50 using the ''kallithea-cli front-end-build'' command.
51 51
52 52
53 53
54 54 Codemirror
55 55 ----------
56 56
57 57 Kallithea uses the Javascript system called
58 58 [Codemirror](http://codemirror.net/), version 4.7.0, which is primarily:
59 59
60 60 Copyright &copy; 2013-2014 by Marijn Haverbeke <marijnh@gmail.com>
61 61
62 62 and licensed under the MIT-permissive license, which is
63 63 [included in this distribution](MIT-Permissive-License.txt).
64 64
65 65 Additional files from upstream Codemirror are copyrighted by various authors
66 66 and licensed under other permissive licenses.
67 67
68 68 It is not distributed with Kallithea, but will be downloaded
69 69 using the ''kallithea-cli front-end-build'' command.
70 70
71 71
72 72
73 73 jQuery
74 74 ------
75 75
76 76 Kallithea uses the Javascript system called
77 77 [jQuery](http://jquery.org/).
78 78
79 79 It is Copyright 2013 jQuery Foundation and other contributors http://jquery.com/ and is under an
80 80 [MIT-permissive license](MIT-Permissive-License.txt).
81 81
82 82 It is not distributed with Kallithea, but will be downloaded
83 83 using the ''kallithea-cli front-end-build'' command.
84 84
85 85
86 86
87 At.js
88 -----
89
90 Kallithea uses the Javascript system called
91 [At.js](http://ichord.github.com/At.js),
92 which can be found together with its Corresponding Source in
93 https://github.com/ichord/At.js at tag v1.5.4.
94
95 It is Copyright 2013 chord.luo@gmail.com and is under an
96 [MIT-permissive license](MIT-Permissive-License.txt).
97
98 It is not distributed with Kallithea, but will be downloaded
99 using the ''kallithea-cli front-end-build'' command.
100
101
102
103 Caret.js
104 --------
105
106 Kallithea uses the Javascript system called
107 [Caret.js](http://ichord.github.com/Caret.js/),
108 which can be found together with its Corresponding Source in
109 https://github.com/ichord/Caret.js at tag v0.3.1.
110
111 It is Copyright 2013 chord.luo@gmail.com and is under an
112 [MIT-permissive license](MIT-Permissive-License.txt).
113
114 It is not distributed with Kallithea, but will be downloaded
115 using the ''kallithea-cli front-end-build'' command.
116
117
118
87 119 DataTables
88 120 ----------
89 121
90 122 Kallithea uses the Javascript system called
91 123 [DataTables](http://www.datatables.net/).
92 124
93 125 It is Copyright 2008-2015 SpryMedia Ltd. and is under an
94 126 [MIT-permissive license](MIT-Permissive-License.txt).
95 127
96 128 It is not distributed with Kallithea, but will be downloaded
97 129 using the ''kallithea-cli front-end-build'' command.
98 130
99 131
100 132
101 133 Mergely
102 134 -------
103 135
104 136 Kallithea incorporates some code from the Javascript system called
105 137 [Mergely](http://www.mergely.com/), version 3.3.9.
106 138 [Mergely's license](http://www.mergely.com/license.php), a
107 139 [copy of which is included in this repository](LICENSE-MERGELY.html),
108 140 is (GPL|LGPL|MPL). Kallithea as GPLv3'd project chooses the GPL arm of that
109 141 tri-license.
110 142
111 143
112 144
113 145 Select2
114 146 -------
115 147
116 148 Kallithea uses the Javascript system called
117 149 [Select2](http://ivaynberg.github.io/select2/), which is:
118 150
119 151 Copyright 2012 Igor Vaynberg (and probably others)
120 152
121 153 and is licensed [under the following license](https://github.com/ivaynberg/select2/blob/master/LICENSE):
122 154
123 155 > This software is licensed under the Apache License, Version 2.0 (the
124 156 > "Apache License") or the GNU General Public License version 2 (the "GPL
125 157 > License"). You may choose either license to govern your use of this
126 158 > software only upon the condition that you accept all of the terms of either
127 159 > the Apache License or the GPL License.
128 160
129 161 A [copy of the Apache License 2.0](Apache-License-2.0.txt) is also included
130 162 in this distribution.
131 163
132 164 Kallithea will take the Apache license fork of the dual license, since
133 165 Kallithea is GPLv3'd.
134 166
135 167 It is not distributed with Kallithea, but will be downloaded
136 168 using the ''kallithea-cli front-end-build'' command.
137 169
138 170
139 171
140 172 Select2-Bootstrap-CSS
141 173 ---------------------
142 174
143 175 Kallithea uses some CSS from a system called
144 176 [Select2-bootstrap-css](https://github.com/t0m/select2-bootstrap-css), which
145 177 is:
146 178
147 179 Copyright &copy; 2013 Tom Terrace (and likely others)
148 180
149 181 and licensed under the MIT-permissive license, which is
150 182 [included in this distribution](MIT-Permissive-License.txt).
151 183
152 184 It is not distributed with Kallithea, but will be downloaded
153 185 using the ''kallithea-cli front-end-build'' command.
154 186
155 187
156 188
157 189 History.js
158 190 ----------
159 191
160 192 Kallithea incorporates some CSS from a system called History.js, which is
161 193
162 194 Copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
163 195
164 196 Redistribution and use in source and binary forms, with or without
165 197 modification, are permitted provided that the following conditions are met:
166 198
167 199 1. Redistributions of source code must retain the above copyright notice,
168 200 this list of conditions and the following disclaimer.
169 201
170 202 2. Redistributions in binary form must reproduce the above copyright notice,
171 203 this list of conditions and the following disclaimer in the documentation
172 204 and/or other materials provided with the distribution.
173 205
174 206 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
175 207 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
176 208 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
177 209 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
178 210 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
179 211 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
180 212 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
181 213 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
182 214 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
183 215 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
184 216 POSSIBILITY OF SUCH DAMAGE.
185 217
186 218
187 219
188 220 YUI
189 221 ---
190 222
191 223 Kallithea incorporates parts of the Javascript system called
192 224 [YUI 2 β€” Yahoo! User Interface Library](http://yui.github.io/yui2/docs/yui_2.9.0_full/),
193 225 which is made available under the [BSD License](http://yuilibrary.com/license/):
194 226
195 227 Copyright &copy; 2013 Yahoo! Inc. All rights reserved.
196 228
197 229 Redistribution and use of this software in source and binary forms, with or
198 230 without modification, are permitted provided that the following conditions are
199 231 met:
200 232
201 233 * Redistributions of source code must retain the above copyright notice, this
202 234 list of conditions and the following disclaimer.
203 235
204 236 * Redistributions in binary form must reproduce the above copyright notice,
205 237 this list of conditions and the following disclaimer in the documentation
206 238 and/or other materials provided with the distribution.
207 239
208 240 * Neither the name of Yahoo! Inc. nor the names of YUI's contributors may be
209 241 used to endorse or promote products derived from this software without
210 242 specific prior written permission of Yahoo! Inc.
211 243
212 244 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
213 245 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
214 246 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
215 247 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
216 248 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
217 249 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
218 250 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
219 251 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
220 252 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
221 253 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
222 254
223 255
224 256 Kallithea includes a minified version of YUI 2.9. To build yui.2.9.js:
225 257
226 258 git clone https://github.com/yui/builder
227 259 git clone https://github.com/yui/yui2
228 260 cd yui2/
229 261 git checkout hudson-yui2-2800
230 262 ln -sf JumpToPageDropDown.js src/paginator/js/JumpToPageDropdown.js # work around inconsistent casing
231 263 rm -f tmp.js
232 264 for m in yahoo event dom animation datasource autocomplete event-delegate; do
233 265 rm -f build/$m/$m.js
234 266 ( cd src/$m && ant build deploybuild ) && sed -e 's,@VERSION@,2.9.0,g' -e 's,@BUILD@,2800,g' build/$m/$m.js >> tmp.js
235 267 done
236 268 java -jar ../builder/componentbuild/lib/yuicompressor/yuicompressor-2.4.4.jar tmp.js -o yui.2.9.js
237 269
238 270 In compliance with GPLv3 the Corresponding Source for this Object Code is made
239 271 available on
240 272 [https://kallithea-scm.org/repos/mirror](https://kallithea-scm.org/repos/mirror).
241 273
242 274
243 275
244 276 YUI Flot
245 277 --------
246 278
247 279 Kallithea incorporates some CSS from a system called
248 280 [Flot](http://code.google.com/p/flot/), which is:
249 281
250 282 Copyright 2006 Google Inc.
251 283
252 284 Licensed under the Apache License, Version 2.0 (the "License");
253 285 you may not use this file except in compliance with the License.
254 286
255 287 A [copy of the Apache License 2.0](Apache-License-2.0.txt) is also included
256 288 in this distribution.
257 289
258 290
259 291
260 292 Flot
261 293 ----
262 294
263 295 Kallithea uses some parts of a Javascript system called
264 296 [Flot](http://www.flotcharts.org/), which is:
265 297
266 298 Copyright (c) 2007-2014 IOLA and Ole Laursen
267 299
268 300 Permission is hereby granted, free of charge, to any person
269 301 obtaining a copy of this software and associated documentation
270 302 files (the "Software"), to deal in the Software without
271 303 restriction, including without limitation the rights to use,
272 304 copy, modify, merge, publish, distribute, sublicense, and/or sell
273 305 copies of the Software, and to permit persons to whom the
274 306 Software is furnished to do so, subject to the following
275 307 conditions:
276 308
277 309 The above copyright notice and this permission notice shall be
278 310 included in all copies or substantial portions of the Software.
279 311
280 312 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
281 313 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
282 314 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
283 315 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
284 316 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
285 317 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
286 318 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
287 319 OTHER DEALINGS IN THE SOFTWARE.
288 320
289 321 It is not distributed with Kallithea, but will be downloaded
290 322 using the ''kallithea-cli front-end-build'' command.
291 323
292 324
293 325
294 326 Icon fonts
295 327 ----------
296 328
297 329 Kallithea incorporates subsets of both
298 330 [Font Awesome](http://fontawesome.io) and
299 331 [GitHub Octicons](https://octicons.github.com) for icons. Font Awesome is:
300 332
301 333 Copyright (c) 2016, Dave Gandy
302 334
303 335 Octicons is:
304 336
305 337 Copyright (c) 2012-2014 GitHub
306 338
307 339 These two sets are distributed under [SIL OFL 1.1](http://scripts.sil.org/OFL)
308 340 and have been combined into one font called "kallithea."
309 341
310 342
311 343 EOF
@@ -1,108 +1,110 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14
15 15 import click
16 16 import kallithea.bin.kallithea_cli_base as cli_base
17 17
18 18 import os
19 19 import shutil
20 20 import subprocess
21 21 import json
22 22
23 23 import kallithea
24 24
25 25 @cli_base.register_command()
26 26 @click.option('--install-deps/--no-install-deps', default=True,
27 27 help='Skip installation of dependencies, via "npm".')
28 28 @click.option('--generate/--no-generate', default=True,
29 29 help='Skip generation of front-end files.')
30 30 def front_end_build(install_deps, generate):
31 31 """Build the front-end.
32 32
33 33 Install required dependencies for the front-end and generate the necessary
34 34 files. This step is complementary to any 'pip install' step which only
35 35 covers Python dependencies.
36 36
37 37 The installation of front-end dependencies happens via the tool 'npm' which
38 38 is expected to be installed already.
39 39 """
40 40 front_end_dir = os.path.abspath(os.path.join(kallithea.__file__, '..', 'front-end'))
41 41 public_dir = os.path.abspath(os.path.join(kallithea.__file__, '..', 'public'))
42 42
43 43 if install_deps:
44 44 click.echo("Running 'npm install' to install front-end dependencies from package.json")
45 45 subprocess.check_call(['npm', 'install'], cwd=front_end_dir)
46 46
47 47 if generate:
48 48 tmp_dir = os.path.join(front_end_dir, 'tmp')
49 49 if not os.path.isdir(tmp_dir):
50 50 os.mkdir(tmp_dir)
51 51
52 52 click.echo("Building CSS styling based on Bootstrap")
53 53 with open(os.path.join(tmp_dir, 'pygments.css'), 'w') as f:
54 54 subprocess.check_call(['pygmentize',
55 55 '-S', 'default',
56 56 '-f', 'html',
57 57 '-a', '.code-highlight'],
58 58 stdout=f)
59 59 lesscpath = os.path.join(front_end_dir, 'node_modules', '.bin', 'lessc')
60 60 lesspath = os.path.join(public_dir, 'less', 'main.less')
61 61 csspath = os.path.join(public_dir, 'css', 'style.css')
62 62 subprocess.check_call([lesscpath, '--relative-urls', '--source-map',
63 63 '--source-map-less-inline', lesspath, csspath],
64 64 cwd=front_end_dir)
65 65
66 66 click.echo("Preparing Bootstrap JS")
67 67 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.js'), os.path.join(public_dir, 'js', 'bootstrap.js'))
68 68
69 click.echo("Preparing jQuery JS with Flot")
69 click.echo("Preparing jQuery JS with Flot, Caret and Atwho")
70 70 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery', 'dist', 'jquery.min.js'), os.path.join(public_dir, 'js', 'jquery.min.js'))
71 71 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.flot', 'jquery.flot.js'), os.path.join(public_dir, 'js', 'jquery.flot.js'))
72 72 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.flot', 'jquery.flot.selection.js'), os.path.join(public_dir, 'js', 'jquery.flot.selection.js'))
73 73 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.flot', 'jquery.flot.time.js'), os.path.join(public_dir, 'js', 'jquery.flot.time.js'))
74 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.caret', 'dist', 'jquery.caret.min.js'), os.path.join(public_dir, 'js', 'jquery.caret.min.js'))
75 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'at.js', 'dist', 'js', 'jquery.atwho.min.js'), os.path.join(public_dir, 'js', 'jquery.atwho.min.js'))
74 76
75 77 click.echo("Preparing DataTables JS")
76 78 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'datatables.net', 'js', 'jquery.dataTables.js'), os.path.join(public_dir, 'js', 'jquery.dataTables.js'))
77 79 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'datatables.net-bs', 'js', 'dataTables.bootstrap.js'), os.path.join(public_dir, 'js', 'dataTables.bootstrap.js'))
78 80
79 81 click.echo("Preparing Select2 JS")
80 82 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2.js'), os.path.join(public_dir, 'js', 'select2.js'))
81 83 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2.png'), os.path.join(public_dir, 'css', 'select2.png'))
82 84 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2x2.png'), os.path.join(public_dir, 'css', 'select2x2.png'))
83 85 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2-spinner.gif'), os.path.join(public_dir, 'css', 'select2-spinner.gif'))
84 86
85 87 click.echo("Preparing CodeMirror JS")
86 88 if os.path.isdir(os.path.join(public_dir, 'codemirror')):
87 89 shutil.rmtree(os.path.join(public_dir, 'codemirror'))
88 90 shutil.copytree(os.path.join(front_end_dir, 'node_modules', 'codemirror'), os.path.join(public_dir, 'codemirror'))
89 91
90 92 click.echo("Generating LICENSES.txt")
91 93 check_licensing_json_path = os.path.join(tmp_dir, 'licensing.json')
92 94 licensing_txt_path = os.path.join(public_dir, 'LICENSES.txt')
93 95 subprocess.check_call([
94 96 os.path.join(front_end_dir, 'node_modules', '.bin', 'license-checker'),
95 97 '--json',
96 98 '--out', check_licensing_json_path,
97 99 ], cwd=front_end_dir)
98 100 with open(check_licensing_json_path) as jsonfile:
99 101 rows = json.loads(jsonfile.read())
100 102 with open(licensing_txt_path, 'w') as out:
101 103 out.write("The Kallithea front-end was built using the following Node modules:\n\n")
102 104 for name_version, values in sorted(rows.items()):
103 105 name, version = name_version.rsplit('@', 1)
104 106 line = "%s from https://www.npmjs.com/package/%s/v/%s\n License: %s\n Repository: %s\n" % (
105 107 name_version, name, version, values['licenses'], values.get('repository', '-'))
106 108 if values.get('copyright'):
107 109 line += " Copyright: %s\n" % (values['copyright'])
108 110 out.write(line + '\n')
@@ -1,19 +1,21 b''
1 1 {
2 2 "name": "kallithea",
3 3 "private": true,
4 4 "dependencies": {
5 "at.js": "1.5.4",
5 6 "bootstrap": "3.3.7",
6 7 "codemirror": "4.7",
7 8 "datatables.net": "1.10.13",
8 9 "datatables.net-bs": "1.10.13",
9 10 "jquery": "1.12.3",
11 "jquery.caret": "0.3.1",
10 12 "jquery.flot": "0.8.3",
11 13 "select2": "3.5.1",
12 14 "select2-bootstrap-css": "1.2.4"
13 15 },
14 16 "devDependencies": {
15 17 "less": "~2.7",
16 18 "less-plugin-clean-css": "~1.5",
17 19 "license-checker": "24.1.0"
18 20 }
19 21 }
@@ -1,1516 +1,1481 b''
1 1 /**
2 2 Kallithea JS Files
3 3 **/
4 4 'use strict';
5 5
6 6 if (typeof console == "undefined" || typeof console.log == "undefined"){
7 7 console = { log: function() {} }
8 8 }
9 9
10 10 /**
11 11 * INJECT .format function into String
12 12 * Usage: "My name is {0} {1}".format("Johny","Bravo")
13 13 * Return "My name is Johny Bravo"
14 14 * Inspired by https://gist.github.com/1049426
15 15 */
16 16 String.prototype.format = function() {
17 17 function format() {
18 18 var str = this;
19 19 var len = arguments.length+1;
20 20 var safe = undefined;
21 21 var arg = undefined;
22 22
23 23 // For each {0} {1} {n...} replace with the argument in that position. If
24 24 // the argument is an object or an array it will be stringified to JSON.
25 25 for (var i=0; i < len; arg = arguments[i++]) {
26 26 safe = typeof arg === 'object' ? JSON.stringify(arg) : arg;
27 27 str = str.replace(RegExp('\\{'+(i-1)+'\\}', 'g'), safe);
28 28 }
29 29 return str;
30 30 }
31 31
32 32 // Save a reference of what may already exist under the property native.
33 33 // Allows for doing something like: if("".format.native) { /* use native */ }
34 34 format.native = String.prototype.format;
35 35
36 36 // Replace the prototype property
37 37 return format;
38 38
39 39 }();
40 40
41 41 String.prototype.strip = function(char) {
42 42 if(char === undefined){
43 43 char = '\\s';
44 44 }
45 45 return this.replace(new RegExp('^'+char+'+|'+char+'+$','g'), '');
46 46 }
47 47
48 48 String.prototype.lstrip = function(char) {
49 49 if(char === undefined){
50 50 char = '\\s';
51 51 }
52 52 return this.replace(new RegExp('^'+char+'+'),'');
53 53 }
54 54
55 55 String.prototype.rstrip = function(char) {
56 56 if(char === undefined){
57 57 char = '\\s';
58 58 }
59 59 return this.replace(new RegExp(''+char+'+$'),'');
60 60 }
61 61
62 62 /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Polyfill
63 63 under MIT license / public domain, see
64 64 https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
65 65 if(!Array.prototype.indexOf) {
66 66 Array.prototype.indexOf = function (searchElement, fromIndex) {
67 67 if ( this === undefined || this === null ) {
68 68 throw new TypeError( '"this" is null or not defined' );
69 69 }
70 70
71 71 var length = this.length >>> 0; // Hack to convert object.length to a UInt32
72 72
73 73 fromIndex = +fromIndex || 0;
74 74
75 75 if (Math.abs(fromIndex) === Infinity) {
76 76 fromIndex = 0;
77 77 }
78 78
79 79 if (fromIndex < 0) {
80 80 fromIndex += length;
81 81 if (fromIndex < 0) {
82 82 fromIndex = 0;
83 83 }
84 84 }
85 85
86 86 for (;fromIndex < length; fromIndex++) {
87 87 if (this[fromIndex] === searchElement) {
88 88 return fromIndex;
89 89 }
90 90 }
91 91
92 92 return -1;
93 93 };
94 94 }
95 95
96 96 /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#Compatibility
97 97 under MIT license / public domain, see
98 98 https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
99 99 if (!Array.prototype.filter)
100 100 {
101 101 Array.prototype.filter = function(fun /*, thisArg */)
102 102 {
103 103 if (this === void 0 || this === null)
104 104 throw new TypeError();
105 105
106 106 var t = Object(this);
107 107 var len = t.length >>> 0;
108 108 if (typeof fun !== "function")
109 109 throw new TypeError();
110 110
111 111 var res = [];
112 112 var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
113 113 for (var i = 0; i < len; i++)
114 114 {
115 115 if (i in t)
116 116 {
117 117 var val = t[i];
118 118
119 119 // NOTE: Technically this should Object.defineProperty at
120 120 // the next index, as push can be affected by
121 121 // properties on Object.prototype and Array.prototype.
122 122 // But that method's new, and collisions should be
123 123 // rare, so use the more-compatible alternative.
124 124 if (fun.call(thisArg, val, i, t))
125 125 res.push(val);
126 126 }
127 127 }
128 128
129 129 return res;
130 130 };
131 131 }
132 132
133 133 /**
134 134 * A customized version of PyRoutes.JS from https://pypi.python.org/pypi/pyroutes.js/
135 135 * which is copyright Stephane Klein and was made available under the BSD License.
136 136 *
137 137 * Usage pyroutes.url('mark_error_fixed',{"error_id":error_id}) // /mark_error_fixed/<error_id>
138 138 */
139 139 var pyroutes = (function() {
140 140 var matchlist = {};
141 141 var sprintf = (function() {
142 142 function get_type(variable) {
143 143 return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
144 144 }
145 145 function str_repeat(input, multiplier) {
146 146 for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */}
147 147 return output.join('');
148 148 }
149 149
150 150 var str_format = function() {
151 151 if (!str_format.cache.hasOwnProperty(arguments[0])) {
152 152 str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
153 153 }
154 154 return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
155 155 };
156 156
157 157 str_format.format = function(parse_tree, argv) {
158 158 var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
159 159 for (i = 0; i < tree_length; i++) {
160 160 node_type = get_type(parse_tree[i]);
161 161 if (node_type === 'string') {
162 162 output.push(parse_tree[i]);
163 163 }
164 164 else if (node_type === 'array') {
165 165 match = parse_tree[i]; // convenience purposes only
166 166 if (match[2]) { // keyword argument
167 167 arg = argv[cursor];
168 168 for (k = 0; k < match[2].length; k++) {
169 169 if (!arg.hasOwnProperty(match[2][k])) {
170 170 throw(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
171 171 }
172 172 arg = arg[match[2][k]];
173 173 }
174 174 }
175 175 else if (match[1]) { // positional argument (explicit)
176 176 arg = argv[match[1]];
177 177 }
178 178 else { // positional argument (implicit)
179 179 arg = argv[cursor++];
180 180 }
181 181
182 182 if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
183 183 throw(sprintf('[sprintf] expecting number but found %s', get_type(arg)));
184 184 }
185 185 switch (match[8]) {
186 186 case 'b': arg = arg.toString(2); break;
187 187 case 'c': arg = String.fromCharCode(arg); break;
188 188 case 'd': arg = parseInt(arg, 10); break;
189 189 case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
190 190 case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
191 191 case 'o': arg = arg.toString(8); break;
192 192 case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
193 193 case 'u': arg = Math.abs(arg); break;
194 194 case 'x': arg = arg.toString(16); break;
195 195 case 'X': arg = arg.toString(16).toUpperCase(); break;
196 196 }
197 197 arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
198 198 pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
199 199 pad_length = match[6] - String(arg).length;
200 200 pad = match[6] ? str_repeat(pad_character, pad_length) : '';
201 201 output.push(match[5] ? arg + pad : pad + arg);
202 202 }
203 203 }
204 204 return output.join('');
205 205 };
206 206
207 207 str_format.cache = {};
208 208
209 209 str_format.parse = function(fmt) {
210 210 var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
211 211 while (_fmt) {
212 212 if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
213 213 parse_tree.push(match[0]);
214 214 }
215 215 else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
216 216 parse_tree.push('%');
217 217 }
218 218 else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
219 219 if (match[2]) {
220 220 arg_names |= 1;
221 221 var field_list = [], replacement_field = match[2], field_match = [];
222 222 if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
223 223 field_list.push(field_match[1]);
224 224 while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
225 225 if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
226 226 field_list.push(field_match[1]);
227 227 }
228 228 else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
229 229 field_list.push(field_match[1]);
230 230 }
231 231 else {
232 232 throw('[sprintf] huh?');
233 233 }
234 234 }
235 235 }
236 236 else {
237 237 throw('[sprintf] huh?');
238 238 }
239 239 match[2] = field_list;
240 240 }
241 241 else {
242 242 arg_names |= 2;
243 243 }
244 244 if (arg_names === 3) {
245 245 throw('[sprintf] mixing positional and named placeholders is not (yet) supported');
246 246 }
247 247 parse_tree.push(match);
248 248 }
249 249 else {
250 250 throw('[sprintf] huh?');
251 251 }
252 252 _fmt = _fmt.substring(match[0].length);
253 253 }
254 254 return parse_tree;
255 255 };
256 256
257 257 return str_format;
258 258 })();
259 259
260 260 var vsprintf = function(fmt, argv) {
261 261 argv.unshift(fmt);
262 262 return sprintf.apply(null, argv);
263 263 };
264 264 return {
265 265 'url': function(route_name, params) {
266 266 var result = route_name;
267 267 if (typeof(params) != 'object'){
268 268 params = {};
269 269 }
270 270 if (matchlist.hasOwnProperty(route_name)) {
271 271 var route = matchlist[route_name];
272 272 // param substitution
273 273 for(var i=0; i < route[1].length; i++) {
274 274 if (!params.hasOwnProperty(route[1][i]))
275 275 throw new Error(route[1][i] + ' missing in "' + route_name + '" route generation');
276 276 }
277 277 result = sprintf(route[0], params);
278 278
279 279 var ret = [];
280 280 //extra params => GET
281 281 for(var param in params){
282 282 if (route[1].indexOf(param) == -1){
283 283 ret.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
284 284 }
285 285 }
286 286 var _parts = ret.join("&");
287 287 if(_parts){
288 288 result = result +'?'+ _parts
289 289 }
290 290 }
291 291
292 292 return result;
293 293 },
294 294 'register': function(route_name, route_tmpl, req_params) {
295 295 if (typeof(req_params) != 'object') {
296 296 req_params = [];
297 297 }
298 298 var keys = [];
299 299 for (var i=0; i < req_params.length; i++) {
300 300 keys.push(req_params[i]);
301 301 }
302 302 matchlist[route_name] = [
303 303 unescape(route_tmpl),
304 304 keys
305 305 ]
306 306 },
307 307 '_routes': function(){
308 308 return matchlist;
309 309 }
310 310 }
311 311 })();
312 312
313 313
314 314 /* Invoke all functions in callbacks */
315 315 var _run_callbacks = function(callbacks){
316 316 if (callbacks !== undefined){
317 317 var _l = callbacks.length;
318 318 for (var i=0;i<_l;i++){
319 319 var func = callbacks[i];
320 320 if(typeof(func)=='function'){
321 321 try{
322 322 func();
323 323 }catch (err){};
324 324 }
325 325 }
326 326 }
327 327 }
328 328
329 329 /**
330 330 * turns objects into GET query string
331 331 */
332 332 var _toQueryString = function(o) {
333 333 if(typeof o !== 'object') {
334 334 return false;
335 335 }
336 336 var _p, _qs = [];
337 337 for(_p in o) {
338 338 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
339 339 }
340 340 return _qs.join('&');
341 341 };
342 342
343 343 /**
344 344 * Load HTML into DOM using Ajax
345 345 *
346 346 * @param $target: load html async and place it (or an error message) here
347 347 * @param success: success callback function
348 348 * @param args: query parameters to pass to url
349 349 */
350 350 function asynchtml(url, $target, success, args){
351 351 if(args===undefined){
352 352 args=null;
353 353 }
354 354 $target.html(_TM['Loading ...']).css('opacity','0.3');
355 355
356 356 return $.ajax({url: url, data: args, headers: {'X-PARTIAL-XHR': '1'}, cache: false, dataType: 'html'})
357 357 .done(function(html) {
358 358 $target.html(html);
359 359 $target.css('opacity','1.0');
360 360 //execute the given original callback
361 361 if (success !== undefined && success) {
362 362 success();
363 363 }
364 364 })
365 365 .fail(function(jqXHR, textStatus, errorThrown) {
366 366 if (textStatus == "abort")
367 367 return;
368 368 $target.html('<span class="bg-danger">ERROR: {0}</span>'.format(textStatus));
369 369 $target.css('opacity','1.0');
370 370 })
371 371 ;
372 372 };
373 373
374 374 var ajaxGET = function(url, success, failure) {
375 375 if(failure === undefined) {
376 376 failure = function(jqXHR, textStatus, errorThrown) {
377 377 if (textStatus != "abort")
378 378 alert("Ajax GET error: " + textStatus);
379 379 };
380 380 }
381 381 return $.ajax({url: url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
382 382 .done(success)
383 383 .fail(failure);
384 384 };
385 385
386 386 var ajaxPOST = function(url, postData, success, failure) {
387 387 postData['_authentication_token'] = _authentication_token;
388 388 var postData = _toQueryString(postData);
389 389 if(failure === undefined) {
390 390 failure = function(jqXHR, textStatus, errorThrown) {
391 391 if (textStatus != "abort")
392 392 alert("Error posting to server: " + textStatus);
393 393 };
394 394 }
395 395 return $.ajax({url: url, data: postData, type: 'POST', headers: {'X-PARTIAL-XHR': '1'}, cache: false})
396 396 .done(success)
397 397 .fail(failure);
398 398 };
399 399
400 400
401 401 /**
402 402 * activate .show_more links
403 403 * the .show_more must have an id that is the the id of an element to hide prefixed with _
404 404 * the parentnode will be displayed
405 405 */
406 406 var show_more_event = function(){
407 407 $('.show_more').click(function(e){
408 408 var el = e.currentTarget;
409 409 $('#' + el.id.substring(1)).hide();
410 410 $(el.parentNode).show();
411 411 });
412 412 };
413 413
414 414
415 415 var _onSuccessFollow = function(target){
416 416 var $target = $(target);
417 417 var $f_cnt = $('#current_followers_count');
418 418 if ($target.hasClass('follow')) {
419 419 $target.removeClass('follow').addClass('following');
420 420 $target.prop('title', _TM['Stop following this repository']);
421 421 if ($f_cnt.html()) {
422 422 var cnt = Number($f_cnt.html())+1;
423 423 $f_cnt.html(cnt);
424 424 }
425 425 } else {
426 426 $target.removeClass('following').addClass('follow');
427 427 $target.prop('title', _TM['Start following this repository']);
428 428 if ($f_cnt.html()) {
429 429 var cnt = Number($f_cnt.html())-1;
430 430 $f_cnt.html(cnt);
431 431 }
432 432 }
433 433 }
434 434
435 435 var toggleFollowingRepo = function(target, follows_repository_id){
436 436 var args = 'follows_repository_id=' + follows_repository_id;
437 437 args += '&amp;_authentication_token=' + _authentication_token;
438 438 $.post(TOGGLE_FOLLOW_URL, args, function(data){
439 439 _onSuccessFollow(target);
440 440 });
441 441 return false;
442 442 };
443 443
444 444 var showRepoSize = function(target, repo_name){
445 445 var args = '_authentication_token=' + _authentication_token;
446 446
447 447 if(!$("#" + target).hasClass('loaded')){
448 448 $("#" + target).html(_TM['Loading ...']);
449 449 var url = pyroutes.url('repo_size', {"repo_name":repo_name});
450 450 $.post(url, args, function(data) {
451 451 $("#" + target).html(data);
452 452 $("#" + target).addClass('loaded');
453 453 });
454 454 }
455 455 return false;
456 456 };
457 457
458 458 /**
459 459 * load tooltips dynamically based on data attributes, used for .lazy-cs changeset links
460 460 */
461 461 var get_changeset_tooltip = function() {
462 462 var $target = $(this);
463 463 var tooltip = $target.data('tooltip');
464 464 if (!tooltip) {
465 465 var raw_id = $target.data('raw_id');
466 466 var repo_name = $target.data('repo_name');
467 467 var url = pyroutes.url('changeset_info', {"repo_name": repo_name, "revision": raw_id});
468 468
469 469 $.ajax(url, {
470 470 async: false,
471 471 success: function(data) {
472 472 tooltip = data["message"];
473 473 }
474 474 });
475 475 $target.data('tooltip', tooltip);
476 476 }
477 477 return tooltip;
478 478 };
479 479
480 480 /**
481 481 * activate tooltips and popups
482 482 */
483 483 var tooltip_activate = function(){
484 484 function placement(p, e){
485 485 if(e.getBoundingClientRect().top > 2*$(window).height()/3){
486 486 return 'top';
487 487 }else{
488 488 return 'bottom';
489 489 }
490 490 }
491 491 $(document).ready(function(){
492 492 $('[data-toggle="tooltip"]').tooltip({
493 493 container: 'body',
494 494 placement: placement
495 495 });
496 496 $('[data-toggle="popover"]').popover({
497 497 html: true,
498 498 container: 'body',
499 499 placement: placement,
500 500 trigger: 'hover',
501 501 template: '<div class="popover cs-popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
502 502 });
503 503 $('.lazy-cs').tooltip({
504 504 title: get_changeset_tooltip,
505 505 placement: placement
506 506 });
507 507 });
508 508 };
509 509
510 510
511 511 /**
512 512 * Quick filter widget
513 513 *
514 514 * @param target: filter input target
515 515 * @param nodes: list of nodes in html we want to filter.
516 516 * @param display_element function that takes current node from nodes and
517 517 * does hide or show based on the node
518 518 */
519 519 var q_filter = (function() {
520 520 var _namespace = {};
521 521 var namespace = function (target) {
522 522 if (!(target in _namespace)) {
523 523 _namespace[target] = {};
524 524 }
525 525 return _namespace[target];
526 526 };
527 527 return function (target, $nodes, display_element) {
528 528 var $nodes = $nodes;
529 529 var $q_filter_field = $('#' + target);
530 530 var F = namespace(target);
531 531
532 532 $q_filter_field.keyup(function (e) {
533 533 clearTimeout(F.filterTimeout);
534 534 F.filterTimeout = setTimeout(F.updateFilter, 600);
535 535 });
536 536
537 537 F.filterTimeout = null;
538 538
539 539 F.updateFilter = function () {
540 540 // Reset timeout
541 541 F.filterTimeout = null;
542 542
543 543 var obsolete = [];
544 544
545 545 var req = $q_filter_field.val().toLowerCase();
546 546
547 547 var showing = 0;
548 548 $nodes.each(function () {
549 549 var n = this;
550 550 var target_element = display_element(n);
551 551 if (req && n.innerHTML.toLowerCase().indexOf(req) == -1) {
552 552 $(target_element).hide();
553 553 }
554 554 else {
555 555 $(target_element).show();
556 556 showing += 1;
557 557 }
558 558 });
559 559
560 560 $('#repo_count').html(showing);
561 561 /* FIXME: don't hardcode */
562 562 }
563 563 }
564 564 })();
565 565
566 566
567 567 /**
568 568 * Comment handling
569 569 */
570 570
571 571 // move comments to their right location, inside new trs
572 572 function move_comments($anchorcomments) {
573 573 $anchorcomments.each(function(i, anchorcomment) {
574 574 var $anchorcomment = $(anchorcomment);
575 575 var target_id = $anchorcomment.data('target-id');
576 576 var $comment_div = _get_add_comment_div(target_id);
577 577 var f_path = $anchorcomment.data('f_path');
578 578 var line_no = $anchorcomment.data('line_no');
579 579 if ($comment_div[0]) {
580 580 $comment_div.append($anchorcomment.children());
581 581 if (f_path && line_no) {
582 582 _comment_div_append_add($comment_div, f_path, line_no);
583 583 } else {
584 584 _comment_div_append_form($comment_div, f_path, line_no);
585 585 }
586 586 } else {
587 587 $anchorcomment.before("<span class='bg-warning'>Comment to {0} line {1} which is outside the diff context:</span>".format(f_path || '?', line_no || '?'));
588 588 }
589 589 });
590 590 linkInlineComments($('.firstlink'), $('.comment:first-child'));
591 591 }
592 592
593 593 // comment bubble was clicked - insert new tr and show form
594 594 function show_comment_form($bubble) {
595 595 var children = $bubble.closest('tr.line').children('[id]');
596 596 var line_td_id = children[children.length - 1].id;
597 597 var $comment_div = _get_add_comment_div(line_td_id);
598 598 var f_path = $bubble.closest('[data-f_path]').data('f_path');
599 599 var parts = line_td_id.split('_');
600 600 var line_no = parts[parts.length-1];
601 601 comment_div_state($comment_div, f_path, line_no, true);
602 602 }
603 603
604 604 // return comment div for target_id - add it if it doesn't exist yet
605 605 function _get_add_comment_div(target_id) {
606 606 var comments_box_id = 'comments-' + target_id;
607 607 var $comments_box = $('#' + comments_box_id);
608 608 if (!$comments_box.length) {
609 609 var html = '<tr><td id="{0}" colspan="3" class="inline-comments"></td></tr>'.format(comments_box_id);
610 610 $('#' + target_id).closest('tr').after(html);
611 611 $comments_box = $('#' + comments_box_id);
612 612 }
613 613 return $comments_box;
614 614 }
615 615
616 616 // Set $comment_div state - showing or not showing form and Add button.
617 617 // An Add button is shown on non-empty forms when no form is shown.
618 618 // The form is controlled by show_form_opt - if undefined, form is only shown for general comments.
619 619 function comment_div_state($comment_div, f_path, line_no, show_form_opt) {
620 620 var show_form = show_form_opt !== undefined ? show_form_opt : !f_path && !line_no;
621 621 var $forms = $comment_div.children('.comment-inline-form');
622 622 var $buttonrow = $comment_div.children('.add-button-row');
623 623 var $comments = $comment_div.children('.comment:not(.submitting)');
624 624 $forms.remove();
625 625 $buttonrow.remove();
626 626 if (show_form) {
627 627 _comment_div_append_form($comment_div, f_path, line_no);
628 628 } else if ($comments.length) {
629 629 _comment_div_append_add($comment_div, f_path, line_no);
630 630 } else {
631 631 $comment_div.parent('tr').remove();
632 632 }
633 633 }
634 634
635 635 // append an Add button to $comment_div and hook it up to show form
636 636 function _comment_div_append_add($comment_div, f_path, line_no) {
637 637 var addlabel = TRANSLATION_MAP['Add Another Comment'];
638 638 var $add = $('<div class="add-button-row"><span class="btn btn-default btn-xs add-button">{0}</span></div>'.format(addlabel));
639 639 $comment_div.append($add);
640 640 $add.children('.add-button').click(function(e) {
641 641 comment_div_state($comment_div, f_path, line_no, true);
642 642 });
643 643 }
644 644
645 645 // append a comment form to $comment_div
646 646 function _comment_div_append_form($comment_div, f_path, line_no) {
647 647 var $form_div = $('#comment-inline-form-template').children()
648 648 .clone()
649 649 .addClass('comment-inline-form');
650 650 $comment_div.append($form_div);
651 651 var $preview = $comment_div.find("div.comment-preview");
652 652 var $form = $comment_div.find("form");
653 653 var $textarea = $form.find('textarea');
654 654
655 655 $form.submit(function(e) {
656 656 e.preventDefault();
657 657
658 658 var text = $textarea.val();
659 659 var review_status = $form.find('input:radio[name=changeset_status]:checked').val();
660 660 var pr_close = $form.find('input:checkbox[name=save_close]:checked').length ? 'on' : '';
661 661 var pr_delete = $form.find('input:checkbox[name=save_delete]:checked').length ? 'delete' : '';
662 662
663 663 if (!text && !review_status && !pr_close && !pr_delete) {
664 664 alert("Please provide a comment");
665 665 return false;
666 666 }
667 667
668 668 if (pr_delete) {
669 669 if (text || review_status || pr_close) {
670 670 alert('Cannot delete pull request while making other changes');
671 671 return false;
672 672 }
673 673 if (!confirm('Confirm to delete this pull request')) {
674 674 return false;
675 675 }
676 676 var comments = $('.comment').size();
677 677 if (comments > 0 &&
678 678 !confirm('Confirm again to delete this pull request with {0} comments'.format(comments))) {
679 679 return false;
680 680 }
681 681 }
682 682
683 683 if (review_status) {
684 684 var $review_status = $preview.find('.automatic-comment');
685 685 var review_status_lbl = $("#comment-inline-form-template input.status_change_radio[value='" + review_status + "']").parent().text().strip();
686 686 $review_status.find('.comment-status-label').text(review_status_lbl);
687 687 $review_status.show();
688 688 }
689 689 $preview.find('.comment-text div').text(text);
690 690 $preview.show();
691 691 $textarea.val('');
692 692 if (f_path && line_no) {
693 693 $form.hide();
694 694 }
695 695
696 696 var postData = {
697 697 'text': text,
698 698 'f_path': f_path,
699 699 'line': line_no,
700 700 'changeset_status': review_status,
701 701 'save_close': pr_close,
702 702 'save_delete': pr_delete
703 703 };
704 704 var success = function(json_data) {
705 705 if (pr_delete) {
706 706 location = json_data['location'];
707 707 } else {
708 708 $comment_div.append(json_data['rendered_text']);
709 709 comment_div_state($comment_div, f_path, line_no);
710 710 linkInlineComments($('.firstlink'), $('.comment:first-child'));
711 711 if ((review_status || pr_close) && !f_path && !line_no) {
712 712 // Page changed a lot - reload it after closing the submitted form
713 713 comment_div_state($comment_div, f_path, line_no, false);
714 714 location.reload(true);
715 715 }
716 716 }
717 717 };
718 718 var failure = function(x, s, e) {
719 719 $preview.removeClass('submitting').addClass('failed');
720 720 var $status = $preview.find('.comment-submission-status');
721 721 $('<span>', {
722 722 'title': e,
723 723 text: _TM['Unable to post']
724 724 }).replaceAll($status.contents());
725 725 $('<div>', {
726 726 'class': 'btn-group'
727 727 }).append(
728 728 $('<button>', {
729 729 'class': 'btn btn-default btn-xs',
730 730 text: _TM['Retry']
731 731 }).click(function() {
732 732 $status.text(_TM['Submitting ...']);
733 733 $preview.addClass('submitting').removeClass('failed');
734 734 ajaxPOST(AJAX_COMMENT_URL, postData, success, failure);
735 735 }),
736 736 $('<button>', {
737 737 'class': 'btn btn-default btn-xs',
738 738 text: _TM['Cancel']
739 739 }).click(function() {
740 740 comment_div_state($comment_div, f_path, line_no);
741 741 })
742 742 ).appendTo($status);
743 743 };
744 744 ajaxPOST(AJAX_COMMENT_URL, postData, success, failure);
745 745 });
746 746
747 747 // add event handler for hide/cancel buttons
748 748 $form.find('.hide-inline-form').click(function(e) {
749 749 comment_div_state($comment_div, f_path, line_no);
750 750 });
751 751
752 752 tooltip_activate();
753 753 if ($textarea.length > 0) {
754 MentionsAutoComplete($textarea, _USERS_AC_DATA);
754 MentionsAutoComplete($textarea);
755 755 }
756 756 if (f_path) {
757 757 $textarea.focus();
758 758 }
759 759 }
760 760
761 761
762 762 function deleteComment(comment_id) {
763 763 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
764 764 var postData = {};
765 765 var success = function(o) {
766 766 $('#comment-'+comment_id).remove();
767 767 // Ignore that this might leave a stray Add button (or have a pending form with another comment) ...
768 768 }
769 769 ajaxPOST(url, postData, success);
770 770 }
771 771
772 772
773 773 /**
774 774 * Double link comments
775 775 */
776 776 var linkInlineComments = function($firstlinks, $comments){
777 777 if ($comments.length > 0) {
778 778 $firstlinks.html('<a href="#{0}">First comment</a>'.format($comments.prop('id')));
779 779 }
780 780 if ($comments.length <= 1) {
781 781 return;
782 782 }
783 783
784 784 $comments.each(function(i, e){
785 785 var prev = '';
786 786 if (i > 0){
787 787 var prev_anchor = $($comments.get(i-1)).prop('id');
788 788 prev = '<a href="#{0}">Previous comment</a>'.format(prev_anchor);
789 789 }
790 790 var next = '';
791 791 if (i+1 < $comments.length){
792 792 var next_anchor = $($comments.get(i+1)).prop('id');
793 793 next = '<a href="#{0}">Next comment</a>'.format(next_anchor);
794 794 }
795 795 $(this).find('.comment-prev-next-links').html(
796 796 '<div class="prev-comment">{0}</div>'.format(prev) +
797 797 '<div class="next-comment">{0}</div>'.format(next));
798 798 });
799 799 }
800 800
801 801 /* activate files.html stuff */
802 802 var fileBrowserListeners = function(node_list_url, url_base){
803 803 var $node_filter = $('#node_filter');
804 804
805 805 var filterTimeout = null;
806 806 var nodes = null;
807 807
808 808 var initFilter = function(){
809 809 $('#node_filter_box_loading').show();
810 810 $('#search_activate_id').hide();
811 811 $('#add_node_id').hide();
812 812 $.ajax({url: node_list_url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
813 813 .done(function(json) {
814 814 nodes = json.nodes;
815 815 $('#node_filter_box_loading').hide();
816 816 $('#node_filter_box').show();
817 817 $node_filter.focus();
818 818 if($node_filter.hasClass('init')){
819 819 $node_filter.val('');
820 820 $node_filter.removeClass('init');
821 821 }
822 822 })
823 823 .fail(function() {
824 824 console.log('fileBrowserListeners initFilter failed to load');
825 825 })
826 826 ;
827 827 }
828 828
829 829 var updateFilter = function(e) {
830 830 return function(){
831 831 // Reset timeout
832 832 filterTimeout = null;
833 833 var query = e.currentTarget.value.toLowerCase();
834 834 var match = [];
835 835 var matches = 0;
836 836 var matches_max = 20;
837 837 if (query != ""){
838 838 for(var i=0;i<nodes.length;i++){
839 839 var pos = nodes[i].name.toLowerCase().indexOf(query);
840 840 if(query && pos != -1){
841 841 matches++
842 842 //show only certain amount to not kill browser
843 843 if (matches > matches_max){
844 844 break;
845 845 }
846 846
847 847 var n = nodes[i].name;
848 848 var t = nodes[i].type;
849 849 var n_hl = n.substring(0,pos)
850 850 + "<b>{0}</b>".format(n.substring(pos,pos+query.length))
851 851 + n.substring(pos+query.length);
852 852 var new_url = url_base.replace('__FPATH__',n);
853 853 match.push('<tr><td><a class="browser-{0}" href="{1}">{2}</a></td><td colspan="5"></td></tr>'.format(t,new_url,n_hl));
854 854 }
855 855 if(match.length >= matches_max){
856 856 match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['Search truncated']));
857 857 break;
858 858 }
859 859 }
860 860 }
861 861 if(query != ""){
862 862 $('#tbody').hide();
863 863 $('#tbody_filtered').show();
864 864
865 865 if (match.length==0){
866 866 match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['No matching files']));
867 867 }
868 868
869 869 $('#tbody_filtered').html(match.join(""));
870 870 }
871 871 else{
872 872 $('#tbody').show();
873 873 $('#tbody_filtered').hide();
874 874 }
875 875 }
876 876 };
877 877
878 878 $('#filter_activate').click(function(){
879 879 initFilter();
880 880 });
881 881 $node_filter.click(function(){
882 882 if($node_filter.hasClass('init')){
883 883 $node_filter.val('');
884 884 $node_filter.removeClass('init');
885 885 }
886 886 });
887 887 $node_filter.keyup(function(e){
888 888 clearTimeout(filterTimeout);
889 889 filterTimeout = setTimeout(updateFilter(e),600);
890 890 });
891 891 };
892 892
893 893
894 894 var initCodeMirror = function(textarea_id, baseUrl, resetUrl){
895 895 var myCodeMirror = CodeMirror.fromTextArea($('#' + textarea_id)[0], {
896 896 mode: "null",
897 897 lineNumbers: true,
898 898 indentUnit: 4,
899 899 autofocus: true
900 900 });
901 901 CodeMirror.modeURL = baseUrl + "/codemirror/mode/%N/%N.js";
902 902
903 903 $('#reset').click(function(e){
904 904 window.location=resetUrl;
905 905 });
906 906
907 907 $('#file_enable').click(function(){
908 908 $('#upload_file_container').hide();
909 909 $('#filename_container').show();
910 910 $('#body').show();
911 911 });
912 912
913 913 $('#upload_file_enable').click(function(){
914 914 $('#upload_file_container').show();
915 915 $('#filename_container').hide();
916 916 $('#body').hide();
917 917 });
918 918
919 919 return myCodeMirror
920 920 };
921 921
922 922 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
923 923 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
924 924 }
925 925
926 926
927 927 var _getIdentNode = function(n){
928 928 //iterate thrugh nodes until matching interesting node
929 929
930 930 if (typeof n == 'undefined'){
931 931 return -1
932 932 }
933 933
934 934 if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
935 935 return n
936 936 }
937 937 else{
938 938 return _getIdentNode(n.parentNode);
939 939 }
940 940 };
941 941
942 942 /* generate links for multi line selects that can be shown by files.html page_highlights.
943 943 * This is a mouseup handler for hlcode from CodeHtmlFormatter and pygmentize */
944 944 var getSelectionLink = function(e) {
945 945 //get selection from start/to nodes
946 946 if (typeof window.getSelection != "undefined") {
947 947 var s = window.getSelection();
948 948
949 949 var from = _getIdentNode(s.anchorNode);
950 950 var till = _getIdentNode(s.focusNode);
951 951
952 952 var f_int = parseInt(from.id.replace('L',''));
953 953 var t_int = parseInt(till.id.replace('L',''));
954 954
955 955 var yoffset = 35;
956 956 var ranges = [parseInt(from.id.replace('L','')), parseInt(till.id.replace('L',''))];
957 957 if (ranges[0] > ranges[1]){
958 958 //highlight from bottom
959 959 yoffset = -yoffset;
960 960 ranges = [ranges[1], ranges[0]];
961 961 }
962 962 var $hl_div = $('div#linktt');
963 963 // if we select more than 2 lines
964 964 if (ranges[0] != ranges[1]){
965 965 if ($hl_div.length) {
966 966 $hl_div.html('');
967 967 } else {
968 968 $hl_div = $('<div id="linktt" class="hl-tip-box">');
969 969 $('body').prepend($hl_div);
970 970 }
971 971
972 972 $hl_div.append($('<a>').html(_TM['Selection Link']).prop('href', location.href.substring(0, location.href.indexOf('#')) + '#L' + ranges[0] + '-'+ranges[1]));
973 973 var xy = $(till).offset();
974 974 $hl_div.css('top', (xy.top + yoffset) + 'px').css('left', xy.left + 'px');
975 975 $hl_div.show();
976 976 }
977 977 else{
978 978 $hl_div.hide();
979 979 }
980 980 }
981 981 };
982 982
983 983 /**
984 984 * Autocomplete functionality
985 985 */
986 986
987 987 // Custom search function for the DataSource of users
988 988 var autocompleteMatchUsers = function (sQuery, myUsers) {
989 989 // Case insensitive matching
990 990 var query = sQuery.toLowerCase();
991 991 var i = 0;
992 992 var l = myUsers.length;
993 993 var matches = [];
994 994
995 995 // Match against each name of each contact
996 996 for (; i < l; i++) {
997 997 var contact = myUsers[i];
998 998 if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
999 999 ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
1000 1000 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
1001 1001 matches[matches.length] = contact;
1002 1002 }
1003 1003 }
1004 1004 return matches;
1005 1005 };
1006 1006
1007 1007 // Custom search function for the DataSource of userGroups
1008 1008 var autocompleteMatchGroups = function (sQuery, myGroups) {
1009 1009 // Case insensitive matching
1010 1010 var query = sQuery.toLowerCase();
1011 1011 var i = 0;
1012 1012 var l = myGroups.length;
1013 1013 var matches = [];
1014 1014
1015 1015 // Match against each name of each group
1016 1016 for (; i < l; i++) {
1017 1017 var matched_group = myGroups[i];
1018 1018 if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
1019 1019 matches[matches.length] = matched_group;
1020 1020 }
1021 1021 }
1022 1022 return matches;
1023 1023 };
1024 1024
1025 1025 // Highlight the snippet if it is found in the full text.
1026 1026 // Snippet must be lowercased already.
1027 1027 var autocompleteHighlightMatch = function (full, snippet) {
1028 1028 var matchindex = full.toLowerCase().indexOf(snippet);
1029 1029 if (matchindex <0)
1030 1030 return full;
1031 1031 return full.substring(0, matchindex)
1032 1032 + '<span class="select2-match">'
1033 1033 + full.substr(matchindex, snippet.length)
1034 1034 + '</span>' + full.substring(matchindex + snippet.length);
1035 1035 };
1036 1036
1037 1037 // Return html snippet for showing the provided gravatar url
1038 1038 var gravatar = function(gravatar_lnk, size, cssclass) {
1039 1039 if (!gravatar_lnk) {
1040 1040 return '';
1041 1041 }
1042 1042 if (gravatar_lnk == 'default') {
1043 1043 return '<i class="icon-user {1}" style="font-size: {0}px;"></i>'.format(size, cssclass);
1044 1044 }
1045 1045 return ('<i class="icon-gravatar {2}"' +
1046 1046 ' style="font-size: {0}px;background-image: url(\'{1}\'); background-size: {0}px"' +
1047 1047 '></i>').format(size, gravatar_lnk, cssclass);
1048 1048 }
1049 1049
1050 1050 var autocompleteGravatar = function(res, gravatar_lnk, size, group) {
1051 1051 var elem;
1052 1052 if (group !== undefined) {
1053 1053 elem = '<i class="perm-gravatar-ac icon-users"></i>';
1054 1054 } else {
1055 1055 elem = gravatar(gravatar_lnk, size, "perm-gravatar-ac");
1056 1056 }
1057 1057 return '<div class="ac-container-wrap">{0}{1}</div>'.format(elem, res);
1058 1058 }
1059 1059
1060 1060 // Custom formatter to highlight the matching letters
1061 1061 var autocompleteFormatter = function (oResultData, sQuery, sResultMatch) {
1062 1062 var query;
1063 1063 if (sQuery && sQuery.toLowerCase) // YAHOO AutoComplete
1064 1064 query = sQuery.toLowerCase();
1065 1065 else if (sResultMatch && sResultMatch.term) // select2 - parameter names doesn't match
1066 1066 query = sResultMatch.term.toLowerCase();
1067 1067
1068 1068 // group
1069 1069 if (oResultData.type == "group") {
1070 1070 return autocompleteGravatar(
1071 1071 "{0}: {1}".format(
1072 1072 _TM['Group'],
1073 1073 autocompleteHighlightMatch(oResultData.grname, query)),
1074 1074 null, null, true);
1075 1075 }
1076 1076
1077 1077 // users
1078 1078 if (oResultData.nname) {
1079 1079 var displayname = autocompleteHighlightMatch(oResultData.nname, query);
1080 1080 if (oResultData.fname && oResultData.lname) {
1081 1081 displayname = "{0} {1} ({2})".format(
1082 1082 autocompleteHighlightMatch(oResultData.fname, query),
1083 1083 autocompleteHighlightMatch(oResultData.lname, query),
1084 1084 displayname);
1085 1085 }
1086 1086
1087 1087 return autocompleteGravatar(displayname, oResultData.gravatar_lnk, oResultData.gravatar_size);
1088 1088 }
1089 1089
1090 1090 return '';
1091 1091 };
1092 1092
1093 1093 var SimpleUserAutoComplete = function ($inputElement) {
1094 1094 $inputElement.select2({
1095 1095 formatInputTooShort: $inputElement.attr('placeholder'),
1096 1096 initSelection : function (element, callback) {
1097 1097 $.ajax({
1098 1098 url: pyroutes.url('users_and_groups_data'),
1099 1099 dataType: 'json',
1100 1100 data: {
1101 1101 key: element.val()
1102 1102 },
1103 1103 success: function(data){
1104 1104 callback(data.results[0]);
1105 1105 }
1106 1106 });
1107 1107 },
1108 1108 minimumInputLength: 1,
1109 1109 ajax: {
1110 1110 url: pyroutes.url('users_and_groups_data'),
1111 1111 dataType: 'json',
1112 1112 data: function(term, page){
1113 1113 return {
1114 1114 query: term
1115 1115 };
1116 1116 },
1117 1117 results: function (data, page){
1118 1118 return data;
1119 1119 },
1120 1120 cache: true
1121 1121 },
1122 1122 formatSelection: autocompleteFormatter,
1123 1123 formatResult: autocompleteFormatter,
1124 1124 escapeMarkup: function(m) { return m; },
1125 1125 id: function(item) { return item.nname; },
1126 1126 });
1127 1127 }
1128 1128
1129 1129 var MembersAutoComplete = function ($inputElement, $typeElement) {
1130 1130
1131 1131 $inputElement.select2({
1132 1132 placeholder: $inputElement.attr('placeholder'),
1133 1133 minimumInputLength: 1,
1134 1134 ajax: {
1135 1135 url: pyroutes.url('users_and_groups_data'),
1136 1136 dataType: 'json',
1137 1137 data: function(term, page){
1138 1138 return {
1139 1139 query: term,
1140 1140 types: 'users,groups'
1141 1141 };
1142 1142 },
1143 1143 results: function (data, page){
1144 1144 return data;
1145 1145 },
1146 1146 cache: true
1147 1147 },
1148 1148 formatSelection: autocompleteFormatter,
1149 1149 formatResult: autocompleteFormatter,
1150 1150 escapeMarkup: function(m) { return m; },
1151 1151 id: function(item) { return item.type == 'user' ? item.nname : item.grname },
1152 1152 }).on("select2-selecting", function(e) {
1153 1153 // e.choice.id is automatically used as selection value - just set the type of the selection
1154 1154 $typeElement.val(e.choice.type);
1155 1155 });
1156 1156 }
1157 1157
1158 var MentionsAutoComplete = function ($inputElement, users_list) {
1159 var $container = $('<div/>').insertAfter($inputElement);
1160
1161 var matchUsers = function (sQuery) {
1162 // use the search string from $inputElement instead of sQuery
1163 if(!$container.data('search')){
1164 // return empty list so the input list isn't shown
1165 return []
1166 }
1167 return autocompleteMatchUsers($container.data('search'), users_list);
1168 }
1169
1170 var datasource = new YAHOO.util.FunctionDataSource(matchUsers);
1171 var mentionsAC = new YAHOO.widget.AutoComplete($inputElement[0], $container[0], datasource);
1172 mentionsAC.useShadow = false;
1173 mentionsAC.resultTypeList = false;
1174 mentionsAC.animVert = false;
1175 mentionsAC.animHoriz = false;
1176 mentionsAC.animSpeed = 0.1;
1177 mentionsAC.suppressInputUpdate = true;
1178 mentionsAC.formatResult = function (oResultData, sQuery, sResultMatch) {
1179 // use the search string from $inputElement instead of sQuery
1180 return autocompleteFormatter(oResultData, $container.data('search'), sResultMatch);
1158 var MentionsAutoComplete = function ($inputElement) {
1159 $inputElement.atwho({
1160 at: "@",
1161 callbacks: {
1162 remoteFilter: function(query, callback) {
1163 $.getJSON(
1164 pyroutes.url('users_and_groups_data'),
1165 {
1166 query: query,
1167 types: 'users'
1168 },
1169 function(data) {
1170 callback(data.results)
1181 1171 }
1182
1183 // Handler for selection of an entry
1184 if(mentionsAC.itemSelectEvent){
1185 mentionsAC.itemSelectEvent.subscribe(function (sType, aArgs) {
1186 var myAC = aArgs[0]; // reference back to the AC instance
1187 var elLI = aArgs[1]; // reference to the selected LI element
1188 var oData = aArgs[2]; // object literal of selected item's result data
1189 myAC.getInputEl().value = $container.data('before') + oData.nname + ' ' + $container.data('after');
1190 _setCaretPosition($(myAC.getInputEl()), myAC.dataSource.before.length + oData.nname.length + 1);
1191 });
1172 );
1173 },
1174 sorter: function(query, items, searchKey) {
1175 return items;
1192 1176 }
1193
1194 // Must match utils2.py MENTIONS_REGEX.
1195 // Operates on a string from char before @ up to cursor.
1196 // Check that the char before @ doesn't look like an email address, and match to end of string.
1197 var mentionRe = new RegExp('(?:^|[^a-zA-Z0-9])@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])$');
1198
1199 $inputElement.keyup(function(e){
1200 var currentMessage = $inputElement.val();
1201 var currentCaretPosition = $inputElement[0].selectionStart;
1202
1203 $container.data('search', '');
1204 var messageBeforeCaret = currentMessage.substr(0, currentCaretPosition);
1205 var lastAtPos = messageBeforeCaret.lastIndexOf('@');
1206 if(lastAtPos >= 0){
1207 // Search from one char before last @ ... if possible
1208 var m = mentionRe.exec(messageBeforeCaret.substr(Math.max(0, lastAtPos - 1)));
1209 if(m){
1210 $container.data('before', currentMessage.substr(0, lastAtPos + 1));
1211 $container.data('search', currentMessage.substr(lastAtPos + 1, currentCaretPosition - lastAtPos - 1));
1212 $container.data('after', currentMessage.substr(currentCaretPosition));
1213 }
1214 }
1177 },
1178 displayTpl: "<li>" + autocompleteGravatar('${fname} ${lname} (${nname})', '${gravatar_lnk}', 16) + "</li>",
1179 insertTpl: "${atwho-at}${nname}"
1215 1180 });
1216 }
1181 };
1217 1182
1218 1183
1219 1184 // Set caret at the given position in the input element
1220 1185 function _setCaretPosition($inputElement, caretPos) {
1221 1186 $inputElement.each(function(){
1222 1187 if(this.createTextRange) { // IE
1223 1188 var range = this.createTextRange();
1224 1189 range.move('character', caretPos);
1225 1190 range.select();
1226 1191 }
1227 1192 else if(this.selectionStart) { // other recent browsers
1228 1193 this.focus();
1229 1194 this.setSelectionRange(caretPos, caretPos);
1230 1195 }
1231 1196 else // last resort - very old browser
1232 1197 this.focus();
1233 1198 });
1234 1199 }
1235 1200
1236 1201
1237 1202 var addReviewMember = function(id,fname,lname,nname,gravatar_link,gravatar_size){
1238 1203 var displayname = nname;
1239 1204 if ((fname != "") && (lname != "")) {
1240 1205 displayname = "{0} {1} ({2})".format(fname, lname, nname);
1241 1206 }
1242 1207 var gravatarelm = gravatar(gravatar_link, gravatar_size, "");
1243 1208 // WARNING: the HTML below is duplicate with
1244 1209 // kallithea/templates/pullrequests/pullrequest_show.html
1245 1210 // If you change something here it should be reflected in the template too.
1246 1211 var element = (
1247 1212 ' <li id="reviewer_{2}">\n'+
1248 1213 ' <span class="reviewers_member">\n'+
1249 1214 ' <input type="hidden" value="{2}" name="review_members" />\n'+
1250 1215 ' <span class="reviewer_status" data-toggle="tooltip" title="not_reviewed">\n'+
1251 1216 ' <i class="icon-circle changeset-status-not_reviewed"></i>\n'+
1252 1217 ' </span>\n'+
1253 1218 (gravatarelm ?
1254 1219 ' {0}\n' :
1255 1220 '')+
1256 1221 ' <span>{1}</span>\n'+
1257 1222 ' <a href="#" class="reviewer_member_remove" onclick="removeReviewMember({2})">\n'+
1258 1223 ' <i class="icon-minus-circled"></i>\n'+
1259 1224 ' </a> (add not saved)\n'+
1260 1225 ' </span>\n'+
1261 1226 ' </li>\n'
1262 1227 ).format(gravatarelm, displayname, id);
1263 1228 // check if we don't have this ID already in
1264 1229 var ids = [];
1265 1230 $('#review_members').find('li').each(function() {
1266 1231 ids.push(this.id);
1267 1232 });
1268 1233 if(ids.indexOf('reviewer_'+id) == -1){
1269 1234 //only add if it's not there
1270 1235 $('#review_members').append(element);
1271 1236 }
1272 1237 }
1273 1238
1274 1239 var removeReviewMember = function(reviewer_id, repo_name, pull_request_id){
1275 1240 var $li = $('#reviewer_{0}'.format(reviewer_id));
1276 1241 $li.find('div div').css("text-decoration", "line-through");
1277 1242 $li.find('input').prop('name', 'review_members_removed');
1278 1243 $li.find('.reviewer_member_remove').replaceWith('&nbsp;(remove not saved)');
1279 1244 }
1280 1245
1281 1246 /* activate auto completion of users as PR reviewers */
1282 1247 var PullRequestAutoComplete = function ($inputElement) {
1283 1248 $inputElement.select2(
1284 1249 {
1285 1250 placeholder: $inputElement.attr('placeholder'),
1286 1251 minimumInputLength: 1,
1287 1252 ajax: {
1288 1253 url: pyroutes.url('users_and_groups_data'),
1289 1254 dataType: 'json',
1290 1255 data: function(term, page){
1291 1256 return {
1292 1257 query: term
1293 1258 };
1294 1259 },
1295 1260 results: function (data, page){
1296 1261 return data;
1297 1262 },
1298 1263 cache: true
1299 1264 },
1300 1265 formatSelection: autocompleteFormatter,
1301 1266 formatResult: autocompleteFormatter,
1302 1267 escapeMarkup: function(m) { return m; },
1303 1268 }).on("select2-selecting", function(e) {
1304 1269 addReviewMember(e.choice.id, e.choice.fname, e.choice.lname, e.choice.nname,
1305 1270 e.choice.gravatar_lnk, e.choice.gravatar_size);
1306 1271 $inputElement.select2("close");
1307 1272 e.preventDefault();
1308 1273 });
1309 1274 }
1310 1275
1311 1276
1312 1277 function addPermAction(perm_type) {
1313 1278 var template =
1314 1279 '<td><input type="radio" value="{1}.none" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
1315 1280 '<td><input type="radio" value="{1}.read" checked="checked" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
1316 1281 '<td><input type="radio" value="{1}.write" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
1317 1282 '<td><input type="radio" value="{1}.admin" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
1318 1283 '<td>' +
1319 1284 '<input class="form-control" id="perm_new_member_name_{0}" name="perm_new_member_name_{0}" value="" type="text" placeholder="{2}">' +
1320 1285 '<input id="perm_new_member_type_{0}" name="perm_new_member_type_{0}" value="" type="hidden">' +
1321 1286 '</td>' +
1322 1287 '<td></td>';
1323 1288 var $last_node = $('.last_new_member').last(); // empty tr between last and add
1324 1289 var next_id = $('.new_members').length;
1325 1290 $last_node.before($('<tr class="new_members">').append(template.format(next_id, perm_type, _TM['Type name of user or member to grant permission'])));
1326 1291 MembersAutoComplete($("#perm_new_member_name_"+next_id), $("#perm_new_member_type_"+next_id));
1327 1292 }
1328 1293
1329 1294 function ajaxActionRevokePermission(url, obj_id, obj_type, field_id, extra_data) {
1330 1295 var success = function (o) {
1331 1296 $('#' + field_id).remove();
1332 1297 };
1333 1298 var failure = function (o) {
1334 1299 alert(_TM['Failed to revoke permission'] + ": " + o.status);
1335 1300 };
1336 1301 var query_params = {};
1337 1302 // put extra data into POST
1338 1303 if (extra_data !== undefined && (typeof extra_data === 'object')){
1339 1304 for(var k in extra_data){
1340 1305 query_params[k] = extra_data[k];
1341 1306 }
1342 1307 }
1343 1308
1344 1309 if (obj_type=='user'){
1345 1310 query_params['user_id'] = obj_id;
1346 1311 query_params['obj_type'] = 'user';
1347 1312 }
1348 1313 else if (obj_type=='user_group'){
1349 1314 query_params['user_group_id'] = obj_id;
1350 1315 query_params['obj_type'] = 'user_group';
1351 1316 }
1352 1317
1353 1318 ajaxPOST(url, query_params, success, failure);
1354 1319 };
1355 1320
1356 1321 /* Multi selectors */
1357 1322
1358 1323 var MultiSelectWidget = function(selected_id, available_id, form_id){
1359 1324 var $availableselect = $('#' + available_id);
1360 1325 var $selectedselect = $('#' + selected_id);
1361 1326
1362 1327 //fill available only with those not in selected
1363 1328 var $selectedoptions = $selectedselect.children('option');
1364 1329 $availableselect.children('option').filter(function(i, e){
1365 1330 for(var j = 0, node; node = $selectedoptions[j]; j++){
1366 1331 if(node.value == e.value){
1367 1332 return true;
1368 1333 }
1369 1334 }
1370 1335 return false;
1371 1336 }).remove();
1372 1337
1373 1338 $('#add_element').click(function(e){
1374 1339 $selectedselect.append($availableselect.children('option:selected'));
1375 1340 });
1376 1341 $('#remove_element').click(function(e){
1377 1342 $availableselect.append($selectedselect.children('option:selected'));
1378 1343 });
1379 1344
1380 1345 $('#'+form_id).submit(function(){
1381 1346 $selectedselect.children('option').each(function(i, e){
1382 1347 e.selected = 'selected';
1383 1348 });
1384 1349 });
1385 1350 }
1386 1351
1387 1352
1388 1353 /**
1389 1354 Branch Sorting callback for select2, modifying the filtered result so prefix
1390 1355 matches come before matches in the line.
1391 1356 **/
1392 1357 var branchSort = function(results, container, query) {
1393 1358 if (query.term) {
1394 1359 return results.sort(function (a, b) {
1395 1360 // Put closed branches after open ones (a bit of a hack ...)
1396 1361 var aClosed = a.text.indexOf("(closed)") > -1,
1397 1362 bClosed = b.text.indexOf("(closed)") > -1;
1398 1363 if (aClosed && !bClosed) {
1399 1364 return 1;
1400 1365 }
1401 1366 if (bClosed && !aClosed) {
1402 1367 return -1;
1403 1368 }
1404 1369
1405 1370 // Put early (especially prefix) matches before later matches
1406 1371 var aPos = a.text.toLowerCase().indexOf(query.term.toLowerCase()),
1407 1372 bPos = b.text.toLowerCase().indexOf(query.term.toLowerCase());
1408 1373 if (aPos < bPos) {
1409 1374 return -1;
1410 1375 }
1411 1376 if (bPos < aPos) {
1412 1377 return 1;
1413 1378 }
1414 1379
1415 1380 // Default sorting
1416 1381 if (a.text > b.text) {
1417 1382 return 1;
1418 1383 }
1419 1384 if (a.text < b.text) {
1420 1385 return -1;
1421 1386 }
1422 1387 return 0;
1423 1388 });
1424 1389 }
1425 1390 return results;
1426 1391 };
1427 1392
1428 1393 var prefixFirstSort = function(results, container, query) {
1429 1394 if (query.term) {
1430 1395 return results.sort(function (a, b) {
1431 1396 // if parent node, no sorting
1432 1397 if (a.children != undefined || b.children != undefined) {
1433 1398 return 0;
1434 1399 }
1435 1400
1436 1401 // Put prefix matches before matches in the line
1437 1402 var aPos = a.text.toLowerCase().indexOf(query.term.toLowerCase()),
1438 1403 bPos = b.text.toLowerCase().indexOf(query.term.toLowerCase());
1439 1404 if (aPos === 0 && bPos !== 0) {
1440 1405 return -1;
1441 1406 }
1442 1407 if (bPos === 0 && aPos !== 0) {
1443 1408 return 1;
1444 1409 }
1445 1410
1446 1411 // Default sorting
1447 1412 if (a.text > b.text) {
1448 1413 return 1;
1449 1414 }
1450 1415 if (a.text < b.text) {
1451 1416 return -1;
1452 1417 }
1453 1418 return 0;
1454 1419 });
1455 1420 }
1456 1421 return results;
1457 1422 };
1458 1423
1459 1424 /* Helper for jQuery DataTables */
1460 1425
1461 1426 var updateRowCountCallback = function updateRowCountCallback($elem, onlyDisplayed) {
1462 1427 return function drawCallback() {
1463 1428 var info = this.api().page.info(),
1464 1429 count = onlyDisplayed === true ? info.recordsDisplay : info.recordsTotal;
1465 1430 $elem.html(count);
1466 1431 }
1467 1432 };
1468 1433
1469 1434
1470 1435 /**
1471 1436 * activate changeset parent/child navigation links
1472 1437 */
1473 1438 var activate_parent_child_links = function(){
1474 1439
1475 1440 $('.parent-child-link').on('click', function(e){
1476 1441 var $this = $(this);
1477 1442 //fetch via ajax what is going to be the next link, if we have
1478 1443 //>1 links show them to user to choose
1479 1444 if(!$this.hasClass('disabled')){
1480 1445 $.ajax({
1481 1446 url: $this.data('ajax-url'),
1482 1447 success: function(data) {
1483 1448 var repo_name = $this.data('reponame');
1484 1449 if(data.results.length === 0){
1485 1450 $this.addClass('disabled');
1486 1451 $this.text(_TM['No revisions']);
1487 1452 }
1488 1453 if(data.results.length === 1){
1489 1454 var commit = data.results[0];
1490 1455 window.location = pyroutes.url('changeset_home', {'repo_name': repo_name, 'revision': commit.raw_id});
1491 1456 }
1492 1457 else if(data.results.length > 1){
1493 1458 $this.addClass('disabled');
1494 1459 $this.addClass('double');
1495 1460 var template =
1496 1461 ($this.data('linktype') == 'parent' ? '<i class="icon-left-open"/> ' : '') +
1497 1462 '<a title="__title__" href="__url__">__rev__</a>' +
1498 1463 ($this.data('linktype') == 'child' ? ' <i class="icon-right-open"/>' : '');
1499 1464 var _html = [];
1500 1465 for(var i = 0; i < data.results.length; i++){
1501 1466 _html.push(template
1502 1467 .replace('__rev__', 'r{0}:{1}'.format(data.results[i].revision, data.results[i].raw_id.substr(0, 6)))
1503 1468 .replace('__title__', data.results[i].message)
1504 1469 .replace('__url__', pyroutes.url('changeset_home', {
1505 1470 'repo_name': repo_name,
1506 1471 'revision': data.results[i].raw_id}))
1507 1472 );
1508 1473 }
1509 1474 $this.html(_html.join('<br/>'));
1510 1475 }
1511 1476 }
1512 1477 });
1513 1478 e.preventDefault();
1514 1479 }
1515 1480 });
1516 1481 }
@@ -1,28 +1,29 b''
1 1 /*!
2 2 * Don't edit the css file directly.
3 3 *
4 4 * Instead, edit the less file(s) and regenerate the css:
5 5 *
6 6 * npm install
7 7 * npm run less
8 8 *
9 9 */
10 10
11 11 /* 3rd party styles */
12 12 @import "node_modules/bootstrap/less/bootstrap.less";
13 13 @import (inline) "node_modules/datatables.net-bs/css/dataTables.bootstrap.css";
14 @import (inline) "node_modules/at.js/dist/css/jquery.atwho.css";
14 15 @import (less) "node_modules/select2/select2.css";
15 16 @import (less) "node_modules/select2-bootstrap-css/select2-bootstrap.css";
16 17 @import (less) "tmp/pygments.css";
17 18 @import (less) "../fontello/css/kallithea.css";
18 19
19 20 /* kallithea styles */
20 21 @import "kallithea-variables.less";
21 22 @import "kallithea-labels.less";
22 23 @import "yui-ac.less";
23 24 @import "kallithea-select2.less";
24 25 @import "kallithea-diff.less";
25 26 @import "style.less";
26 27
27 28 /* finally, import the optional theme file with local customizations */
28 29 @import (optional) "theme.less";
@@ -1,931 +1,937 b''
1 1 body {
2 2 background: url("../images/background.png") repeat scroll 0 0 #B0B0B0;
3 3 }
4 4
5 5 /* pseude content that should not be selected or copied by the user */
6 6 [data-pseudo-content]:before {
7 7 content: attr(data-pseudo-content);
8 8 }
9 9
10 10 /* class for texts where newlines should be preserved, for very light-weight ascii art markup (like pull request descriptions) */
11 11 .formatted-fixed {
12 12 font-family: @font-family-monospace;
13 13 white-space: pre-wrap;
14 14 }
15 15
16 16 /* use monospace for changeset hashes */
17 17 .changeset_hash {
18 18 font-family: @font-family-monospace;
19 19 }
20 20
21 21 /* Note: class 'icon-empty' or 'icon-gravatar' can be used to get icon-ish styling without an actual glyph */
22 22 i[class^='icon-empty'],
23 23 i[class^='icon-gravatar'] {
24 24 background-repeat: no-repeat;
25 25 background-position: center;
26 26 display: inline-block;
27 27 min-width: 16px;
28 28 min-height: 16px;
29 29 margin: -2px 0 -4px 0;
30 30 }
31 31
32 32 .inline-comments-general.show-general-status .hidden.general-only {
33 33 display: block !important;
34 34 }
35 35 .truncate {
36 36 white-space: nowrap;
37 37 overflow: hidden;
38 38 text-overflow: ellipsis;
39 39 -o-text-overflow: ellipsis;
40 40 -ms-text-overflow: ellipsis;
41 41 }
42 42 .truncate.autoexpand:hover {
43 43 overflow: visible;
44 44 }
45 45
46 46 /* show comment anchors when hovering over panel-heading */
47 47 a.permalink {
48 48 visibility: hidden;
49 49 }
50 50 .panel-heading:hover .permalink {
51 51 visibility: visible;
52 52 }
53 53
54 54 .navbar-inverse {
55 55 border: none;
56 56 }
57 57
58 58 /* logo */
59 59 nav.navbar.mainmenu > .navbar-header > .navbar-brand {
60 60 font-size: 20px;
61 61 padding-top: 12px;
62 62 > .branding:before {
63 63 content: "";
64 64 display: inline-block;
65 65 margin-right: .2em;
66 66 background-image: url(@kallithea-logo-url);
67 67 width: @kallithea-logo-width;
68 68 height: @kallithea-logo-height;
69 69 margin-bottom: -@kallithea-logo-bottom;
70 70 margin-top: -12px;
71 71 }
72 72 }
73 73
74 74 /* code highlighting */
75 75 /* don't use bootstrap style for code blocks */
76 76 .code-highlighttable pre {
77 77 background: inherit;
78 78 border: 0;
79 79 }
80 80
81 81 /* every direct child of a panel, that is not .panel-heading, should auto
82 82 * overflow to prevent overflowing of elements like text boxes and tables */
83 83 .panel > :not(.panel-heading){
84 84 overflow-x: auto;
85 85 min-height: 0.01%;
86 86 }
87 87
88 88 /* allow other exceptions to automatic overflow-x */
89 89 .panel > .overflow-x-visible {
90 90 overflow-x: visible;
91 91 }
92 92
93 93 /* margin below top level panels */
94 94 #main > .panel {
95 95 margin-bottom: @kallithea-panel-margin;
96 96 }
97 97
98 98 /* search highlighting */
99 99 div.search-code-body pre .match {
100 100 background-color: @highlight-color;
101 101 }
102 102 div.search-code-body pre .break {
103 103 background-color: @highlight-line-color;
104 104 width: 100%;
105 105 display: block;
106 106 }
107 107
108 108 /* use @alert-danger-text for form error messages and .alert-danger for the input element */
109 109 .form-group .error-message {
110 110 color: @alert-danger-text;
111 111 display: inline-block;
112 112 padding-top: 5px;
113 113 &:empty{
114 114 display: none;
115 115 }
116 116 }
117 117 input.error {
118 118 .alert-danger;
119 119 }
120 120
121 121 /* datatable */
122 122 .dataTables_left {
123 123 .pull-left;
124 124 }
125 125 .dataTables_right {
126 126 .pull-right;
127 127 }
128 128
129 129 /* make all datatable paginations small */
130 130 .dataTables_paginate .pagination {
131 131 .pagination-sm;
132 132 }
133 133
134 134 /* show column sort icons in our font ... and before column header */
135 135 table.dataTable {
136 136 .sorting_asc:before {
137 137 font-family: "kallithea";
138 138 content: "\23f6";
139 139 padding-right: 8px;
140 140 }
141 141 .sorting_desc:before {
142 142 font-family: "kallithea";
143 143 content: "\23f7";
144 144 padding-right: 8px;
145 145 }
146 146 .sorting:before {
147 147 font-family: "kallithea";
148 148 content: "\2195";
149 149 padding-right: 8px;
150 150 opacity: 0.5;
151 151 }
152 152 .sorting_asc:after,
153 153 .sorting_desc:after,
154 154 .sorting:after {
155 155 content: "" !important;
156 156 }
157 157 }
158 158
159 159 /* _dt_elements.html styling - some submit buttons have their own form but should still be shown inline */
160 160 table.dataTable td > .btn + form {
161 161 display: inline;
162 162 }
163 163
164 164 table.dataTable .dt_repo_pending {
165 165 opacity: 0.5;
166 166 }
167 167
168 168 /* language bars (summary page) */
169 169 #lang_stats {
170 170 .progress-bar {
171 171 min-width: 15px;
172 172 border-top-right-radius: 8px;
173 173 border-bottom-right-radius: 8px;
174 174 }
175 175 td {
176 176 padding: 1px 0 !important;
177 177 }
178 178 }
179 179
180 180 /* use pointer cursor for expand_commit */
181 181 .expand_commit .icon-align-left {
182 182 cursor: pointer;
183 183 color: #999;
184 184 }
185 185
186 186 /* don't break author, date and comment cells into multiple lines in changeset table */
187 187 table.changesets {
188 188 .author,
189 189 .date,
190 190 .comments {
191 191 white-space: nowrap;
192 192 }
193 193 }
194 194
195 195 /* textareas should be at least 100px high and 400px wide */
196 196 textarea.form-control {
197 197 font-family: @font-family-monospace;
198 198 min-height: 100px;
199 199 min-width: 400px;
200 200 }
201 201
202 202 /* add some space between the code-browser icons and the file names */
203 203 .browser-dir > i[class^='icon-'],
204 204 .submodule-dir > i[class^='icon-'],
205 205 .browser-file > i[class^='icon-'] {
206 206 padding-right: 0.3em;
207 207 }
208 208
209 209 div.panel-primary {
210 210 border: none;
211 211 }
212 212
213 213 /* no extra vertical margin */
214 214 #content div.panel ul.pagination {
215 215 margin: 0;
216 216 }
217 217
218 218 /* remove margin below footer */
219 219 .navbar.footer {
220 220 margin-bottom: 0;
221 221 }
222 222
223 223 .user-menu {
224 224 padding: 0 !important;
225 225 }
226 226 #quick_login {
227 227 width: 360px;
228 228 margin-top: 15px;
229 229 min-height: 110px;
230 230 }
231 231 #quick_login input#username,
232 232 #quick_login input#password {
233 233 display: block;
234 234 margin: 5px 0 10px;
235 235 }
236 236 #quick_login .password_forgotten a,
237 237 #quick_login .register a {
238 238 padding: 0 !important;
239 239 line-height: 25px !important;
240 240 float: left;
241 241 clear: both;
242 242 }
243 243 #quick_login .submit {
244 244 float: right;
245 245 }
246 246 #quick_login .submit input#sign_in {
247 247 margin-top: 5px;
248 248 }
249 249 #quick_login > .pull-left {
250 250 width: 170px;
251 251 }
252 252 #quick_login > .pull-right {
253 253 width: 140px;
254 254 }
255 255 #quick_login .full_name {
256 256 font-weight: bold;
257 257 padding: 3px;
258 258 }
259 259 #quick_login .email {
260 260 padding: 3px 3px 3px 0;
261 261 }
262 262 #quick_login :not(input) {
263 263 color: @kallithea-theme-inverse-color;
264 264 padding-bottom: 3px;
265 265 }
266 266
267 267 #journal .journal_user {
268 268 color: #747474;
269 269 font-size: 14px;
270 270 font-weight: bold;
271 271 height: 30px;
272 272 }
273 273 #journal .journal_user.deleted {
274 274 color: #747474;
275 275 font-size: 14px;
276 276 font-weight: normal;
277 277 height: 30px;
278 278 font-style: italic;
279 279 }
280 280 #journal .journal_icon {
281 281 clear: both;
282 282 float: left;
283 283 padding-right: 4px;
284 284 padding-top: 3px;
285 285 }
286 286 #journal .journal_action {
287 287 padding-top: 4px;
288 288 min-height: 2px;
289 289 float: left;
290 290 }
291 291 #journal .journal_action_params {
292 292 clear: left;
293 293 padding-left: 22px;
294 294 }
295 295 #journal .date {
296 296 clear: both;
297 297 color: #777777;
298 298 font-size: 11px;
299 299 padding-left: 22px;
300 300 }
301 301 #journal .journal_repo_name {
302 302 font-weight: bold;
303 303 font-size: 1.1em;
304 304 }
305 305 #journal .compare_view {
306 306 padding: 5px 0px 5px 0px;
307 307 width: 95px;
308 308 }
309 309 .trending_language_tbl,
310 310 .trending_language_tbl td {
311 311 border: 0 !important;
312 312 margin: 0 !important;
313 313 padding: 0 !important;
314 314 }
315 315 .trending_language_tbl,
316 316 .trending_language_tbl tr {
317 317 border-spacing: 1px;
318 318 }
319 319 h3.files_location {
320 320 font-size: 1.8em;
321 321 font-weight: 700;
322 322 border-bottom: none !important;
323 323 margin: 10px 0 !important;
324 324 }
325 325 .file_history {
326 326 padding-top: 10px;
327 327 font-size: 16px;
328 328 }
329 329 .file_author {
330 330 float: left;
331 331 }
332 332 .file_author .item {
333 333 float: left;
334 334 padding: 5px;
335 335 color: #888;
336 336 }
337 337 table#updaterevs-table tr.mergerow,
338 338 table#updaterevs-table tr.out-of-range,
339 339 table#changesets tr.mergerow,
340 340 table#changesets tr.out-of-range {
341 341 opacity: 0.6;
342 342 }
343 343 .issue-tracker-link {
344 344 color: #3F6F9F;
345 345 font-weight: bold !important;
346 346 }
347 347 /* changeset statuses (must be the same name as the status) */
348 348 .changeset-status-not_reviewed {
349 349 color: #bababa;
350 350 }
351 351 .changeset-status-approved {
352 352 color: #81ba51;
353 353 }
354 354 .changeset-status-rejected {
355 355 color: #d06060;
356 356 }
357 357 .changeset-status-under_review {
358 358 color: #ffc71e;
359 359 }
360 360
361 361 #repo_size {
362 362 display: block;
363 363 margin-top: 4px;
364 364 color: #666;
365 365 float: right;
366 366 }
367 367 .currently_following {
368 368 padding-left: 10px;
369 369 padding-bottom: 5px;
370 370 }
371 371 #switch_repos {
372 372 position: absolute;
373 373 height: 25px;
374 374 z-index: 1;
375 375 }
376 376 #switch_repos select {
377 377 min-width: 150px;
378 378 max-height: 250px;
379 379 z-index: 1;
380 380 }
381 381 table#permissions_manage span.private_repo_msg {
382 382 font-size: 0.8em;
383 383 opacity: 0.6;
384 384 }
385 385 table#permissions_manage td.private_repo_msg {
386 386 font-size: 0.8em;
387 387 }
388 388 table#permissions_manage tr#add_perm_input td {
389 389 vertical-align: middle;
390 390 }
391 391 div.gravatar {
392 392 float: left;
393 393 background-color: #FFF;
394 394 margin-right: 0.7em;
395 395 padding: 1px 1px 1px 1px;
396 396 line-height: 0;
397 397 border-radius: 3px;
398 398 }
399 399 div.gravatar img {
400 400 border-radius: 2px;
401 401 }
402 402 .panel-body.settings .nav-pills > :not(.active) > a {
403 403 color: inherit;
404 404 }
405 405 .panel-body.no-padding {
406 406 padding: 0;
407 407 }
408 408 .panel-body ~ .panel-body {
409 409 padding-top: 0;
410 410 }
411 411 .panel-body.no-padding ~ .panel-body {
412 412 padding-top: 15px;
413 413 }
414 414 .panel-body > :last-child {
415 415 margin-bottom: 0;
416 416 }
417 417 .panel-body.settings .text-muted {
418 418 margin: 5px 0;
419 419 }
420 420 ins,
421 421 div.options a:hover {
422 422 text-decoration: none;
423 423 }
424 424 img,
425 425 nav.navbar #quick li a:hover span.normal,
426 426 #clone_url,
427 427 #clone_url_id {
428 428 border: none;
429 429 }
430 430 img.icon,
431 431 .right .merge img {
432 432 vertical-align: bottom;
433 433 }
434 434 #content div.panel div.panel-heading ul.links,
435 435 #content div.panel div.message div.dismiss {
436 436 float: right;
437 437 margin: 0;
438 438 padding: 0;
439 439 }
440 440 nav.navbar #home,
441 441 #content div.panel ul.left,
442 442 #content div.panel ol.left,
443 443 div#commit_history,
444 444 div#legend_data,
445 445 div#legend_container,
446 446 div#legend_choices {
447 447 float: left;
448 448 }
449 449
450 450 /* set size for statistics charts */
451 451 #commit_history {
452 452 width: 450px;
453 453 height: 300px;
454 454 }
455 455 #overview {
456 456 clear: both;
457 457 width: 450px;
458 458 height: 100px;
459 459 }
460 460
461 461 #content #left #menu ul.closed,
462 462 #content #left #menu li ul.collapsed,
463 463 .yui-tt-shadow {
464 464 display: none;
465 465 }
466 466 #content #left #menu ul.opened,
467 467 #content #left #menu li ul.expanded {
468 468 display: block !important;
469 469 }
470 470
471 471 #content div.panel ol.lower-roman,
472 472 #content div.panel ol.upper-roman,
473 473 #content div.panel ol.lower-alpha,
474 474 #content div.panel ol.upper-alpha,
475 475 #content div.panel ol.decimal {
476 476 margin: 10px 24px 10px 44px;
477 477 }
478 478
479 479 div.form div.form-group div.button input,
480 480 #content div.panel div.form div.buttons input,
481 481 div.form div.buttons input,
482 482 #content div.panel div.action div.button input {
483 483 font-size: 11px;
484 484 font-weight: 700;
485 485 margin: 0;
486 486 }
487 487 div.form div.form-group div.highlight,
488 488 #content div.panel div.form div.buttons div.highlight {
489 489 display: inline;
490 490 }
491 491 #content div.panel table td.user,
492 492 #content div.panel table td.address {
493 493 width: 10%;
494 494 text-align: center;
495 495 }
496 496 #content div.panel div.action div.button {
497 497 text-align: right;
498 498 margin: 6px 0 0;
499 499 padding: 0;
500 500 }
501 501 .ac .match {
502 502 font-weight: 700;
503 503 padding-top: 5px;
504 504 padding-bottom: 5px;
505 505 }
506 506 .q_filter_box {
507 507 border-radius: 4px;
508 508 border: 0 none;
509 509 margin-bottom: -4px;
510 510 margin-top: -4px;
511 511 padding-left: 3px;
512 512 }
513 513 #node_filter {
514 514 border: 0px solid #545454;
515 515 color: #AAAAAA;
516 516 padding-left: 3px;
517 517 }
518 518 /** comment main **/
519 519 .comment .panel,
520 520 .comment-inline-form {
521 521 max-width: 978px;
522 522 }
523 523 .comment .panel-body {
524 524 background-color: #FAFAFA;
525 525 }
526 526 .comments-number {
527 527 padding: 10px 0;
528 528 color: #666;
529 529 }
530 530 .automatic-comment {
531 531 font-style: italic;
532 532 }
533 533 /** comment form **/
534 534 .status-block {
535 535 margin: 5px;
536 536 clear: both;
537 537 }
538 538 .panel-heading .pull-left input[type=checkbox],
539 539 .panel-heading .pull-right input[type=checkbox] {
540 540 position: relative;
541 541 top: 4px;
542 542 margin: -10px 2px 0;
543 543 }
544 544 /** comment inline **/
545 545 .inline-comments {
546 546 padding: 5px;
547 547 }
548 548 .inline-comments .comments-number {
549 549 padding: 0px 0px 10px 0px;
550 550 }
551 551 input.status_change_checkbox,
552 552 input.status_change_radio {
553 553 margin: 0 0 5px 15px;
554 554 }
555 555 @keyframes animated-comment-background {
556 556 0% {
557 557 background-position: 0 0;
558 558 }
559 559 100% {
560 560 background-position: 20px 0;
561 561 }
562 562 }
563 563 .comment-preview.failed .user,
564 564 .comment-preview.failed .panel-body {
565 565 color: #666;
566 566 }
567 567 .comment-preview .comment-submission-status {
568 568 float: right;
569 569 }
570 570 .comment-preview .comment-submission-status .btn-group {
571 571 margin-left: 10px;
572 572 }
573 573 .comment-preview.submitting .panel-body {
574 574 background-image: linear-gradient(-45deg, #FAFAFA, #FAFAFA 25%, #FFF 25%, #FFF 50%, #FAFAFA 50%, #FAFAFA 75%, #FFF 75%, #FFF 100%);
575 575 background-size: 20px 20px;
576 576 animation: animated-comment-background 0.4s linear infinite;
577 577 }
578 578 /****
579 579 PULL REQUESTS
580 580 *****/
581 581 div.pr-details-title.closed {
582 582 color: #555;
583 583 background: #eee;
584 584 }
585 585 div.pr {
586 586 margin: 0px 15px;
587 587 padding: 4px 4px;
588 588 }
589 589 tr.pr-closed td {
590 590 background-color: #eee !important;
591 591 color: #555 !important;
592 592 }
593 593 span.pr-closed-tag {
594 594 margin-bottom: 1px;
595 595 margin-right: 1px;
596 596 padding: 1px 3px;
597 597 font-size: 10px;
598 598 color: @kallithea-theme-main-color;
599 599 white-space: nowrap;
600 600 border-radius: 4px;
601 601 border: 1px solid #d9e8f8;
602 602 line-height: 1.5em;
603 603 }
604 604 #s2id_org_ref,
605 605 #s2id_other_ref,
606 606 #s2id_org_repo,
607 607 #s2id_other_repo {
608 608 min-width: 150px;
609 609 margin: 5px;
610 610 }
611 611 #pr-summary > .pr-not-edit {
612 612 min-height: 50px !important;
613 613 }
614 614 /* make 'next iteration' changeset table smaller and scrollable */
615 615 #pr-summary #updaterevs {
616 616 max-height: 200px;
617 617 overflow-y: auto;
618 618 overflow-x: hidden;
619 619 }
620 620
621 621 /****
622 622 PERMS
623 623 *****/
624 624 .perm-gravatar-ac {
625 625 vertical-align: middle;
626 626 padding: 2px;
627 627 width: 14px;
628 628 height: 14px;
629 629 }
630 630
631 631 /* avoid gaps between the navbar and browser */
632 632 .navbar.mainmenu {
633 633 border-top-left-radius: 0;
634 634 border-top-right-radius: 0;
635 635 }
636 636 .navbar.footer {
637 637 border-bottom-left-radius: 0;
638 638 border-bottom-right-radius: 0;
639 639 }
640 640
641 641 /* show some context of link targets - but only works when the link target
642 642 can be extended with any visual difference */
643 643 div.comment:target:before {
644 644 display: block;
645 645 height: 100px;
646 646 margin: -100px 0 0;
647 647 content: "";
648 648 }
649 649 div.comment:target > .panel {
650 650 border: solid 2px #ee0 !important;
651 651 }
652 652 .lineno:target a {
653 653 border: solid 2px #ee0 !important;
654 654 margin: -2px;
655 655 }
656 656 .btn-image-diff-show,
657 657 .btn-image-diff-swap {
658 658 margin: 5px;
659 659 }
660 660 .img-diff {
661 661 max-width: 45%;
662 662 height: auto;
663 663 margin: 5px;
664 664 /* http://lea.verou.me/demos/css3-patterns.html */
665 665 background-image: linear-gradient(45deg, #888 25%, transparent 25%, transparent), linear-gradient(-45deg, #888 25%, transparent 25%, transparent), linear-gradient(45deg, transparent 75%, #888 75%), linear-gradient(-45deg, transparent 75%, #888 75%);
666 666 background-size: 10px 10px;
667 667 background-color: #999;
668 668 }
669 669 .img-preview {
670 670 max-width: 100%;
671 671 height: auto;
672 672 margin: 5px;
673 673 }
674 674 div.comment-prev-next-links div.prev-comment,
675 675 div.comment-prev-next-links div.next-comment {
676 676 display: inline-block;
677 677 min-width: 150px;
678 678 margin: 3px 6px;
679 679 }
680 680 #comments-general-comments div.comment-prev-next-links div.prev-comment,
681 681 #comments-general-comments div.comment-prev-next-links div.next-comment {
682 682 margin-left: 0;
683 683 }
684 684
685 685 /* changelog graph */
686 686 #graph_nodes,
687 687 #updaterevs-graph {
688 688 .make-xs-column(1);
689 689 height: 0;
690 690 }
691 691 #graph_content,
692 692 #graph_content_pr,
693 693 #updaterevs-table {
694 694 .make-xs-column-offset(1);
695 695 .make-xs-column(11);
696 696 }
697 697
698 698 /* use bootstrap grid columns for centered columns */
699 699 .centered-column {
700 700 .make-sm-column-offset(3);
701 701 .make-sm-column(6);
702 702 .form {
703 703 .form-horizontal;
704 704 .form-group > label {
705 705 .make-sm-column(4);
706 706 }
707 707 .form-group > div {
708 708 .make-sm-column(8);
709 709 }
710 710 .form-group > div:first-child { /* in case there is no label */
711 711 .make-sm-column-offset(4);
712 712 .make-sm-column(8);
713 713 }
714 714 }
715 715 }
716 716
717 717 /* use columns and form-horizontal for settings pages ... on wide screens */
718 718 @media (min-width: @screen-sm-min) {
719 719 .settings {
720 720 max-width: @container-md;
721 721 > ul.nav-stacked {
722 722 .make-sm-column(2);
723 723 max-width: (@container-md/12)*2;
724 724 }
725 725 > div {
726 726 .make-sm-column(10);
727 727 max-width: (@container-md/12)*10;
728 728 }
729 729 .form {
730 730 .form-horizontal;
731 731 }
732 732 .form-group {
733 733 .clearfix;
734 734 > label {
735 735 .make-xs-column(3);
736 736 overflow: hidden;
737 737 text-overflow: ellipsis;
738 738 input {
739 739 width: 100%;
740 740 }
741 741 }
742 742 > div {
743 743 .make-xs-column(9);
744 744 }
745 745 .buttons {
746 746 .make-xs-column-offset(3);
747 747 }
748 748 }
749 749 }
750 750 }
751 751
752 752 /* use columns and form-horizontal for summary page */
753 753 #summary {
754 754 max-width: @container-md;
755 755 .form-horizontal;
756 756 .make-sm-column(10);
757 757 .form-group > label {
758 758 .make-sm-column(2);
759 759 }
760 760 .form-group > div {
761 761 .make-sm-column(10);
762 762 }
763 763 }
764 764 #summary-menu-stats {
765 765 .make-sm-column(2);
766 766 }
767 767
768 768 /* use columns and form-horizontal for pull request page */
769 769 .pr-box {
770 770 .make-sm-column(9);
771 771 max-width: @container-md;
772 772 #pr-summary {
773 773 .form-horizontal;
774 774 .form-group > label {
775 775 .make-sm-column(3);
776 776 }
777 777 .form-group > div {
778 778 .make-sm-column(9);
779 779 }
780 780 .form-group > .buttons {
781 781 .make-sm-column-offset(3);
782 782 .make-sm-column(9);
783 783 }
784 784 }
785 785 }
786 786 .pr-reviewers-box {
787 787 .make-sm-column(3);
788 788 }
789 789
790 790 /* repo table icons */
791 791 #repos_list_wrap_wrapper {
792 792 /* make icon-folder and repotag the same width */
793 793 .icon-folder:before {
794 794 margin: 0; // default margin would otherwise add to the total width
795 795 width: 24px;
796 796 text-align: left;
797 797 }
798 798 .label-repo {
799 799 display: inline-block;
800 800 width: 24px;
801 801 }
802 802 }
803 803
804 804 /* changelog table columns */
805 805 .table#changesets {
806 806 table-layout: fixed;
807 807 td {
808 808 overflow: hidden;
809 809 text-overflow: ellipsis;
810 810 white-space: nowrap;
811 811 vertical-align: baseline;
812 812 }
813 813 .checkbox-column {
814 814 width: 24px;
815 815 /* the optional second checkbox will be inline-block but should wrap to a new line */
816 816 white-space: normal;
817 817 > input[type=checkbox] {
818 818 margin-top: inherit;
819 819 vertical-align: text-bottom;
820 820 }
821 821 }
822 822 .changeset-logical-index {
823 823 color: @gray-light;
824 824 font-style: italic;
825 825 font-size: 85%;
826 826 text-align: right;
827 827 overflow: visible;
828 828 }
829 829 .changeset-logical-index,
830 830 .expand_commit,
831 831 .status {
832 832 width: 28px;
833 833 }
834 834 .author {
835 835 width: 200px;
836 836 @media (max-width: @screen-sm-max) {
837 837 width: 120px;
838 838 }
839 839 @media (max-width: @screen-xs-max) {
840 840 width: 20px;
841 841 /* keep gravatar but hide name on tiny screens to give important columns more room */
842 842 span {
843 843 .hidden;
844 844 }
845 845 }
846 846 }
847 847 .hash {
848 848 .small;
849 849 width: 110px;
850 850 @media (max-width: @screen-xs-max) {
851 851 width: 48px;
852 852 }
853 853 }
854 854 .date {
855 855 .small;
856 856 width: 100px;
857 857 }
858 858 /* hide on small screens to give important columns more room */
859 859 .status,
860 860 .expand_commit,
861 861 .comments,
862 862 .extra-container {
863 863 .hidden-xs;
864 864 }
865 865 .mid > .log-container {
866 866 position: relative;
867 867 overflow: hidden;
868 868 > .extra-container {
869 869 position: absolute;
870 870 top: 0;
871 871 right: 0;
872 872 background: white;
873 873 box-shadow: -10px 0px 10px 0px white;
874 874 }
875 875 }
876 876 }
877 877
878 878 /* undo Bootstrap chrome/webkit blue outline on focus in navbar */
879 879 .navbar-inverse .navbar-nav > li > a:focus {
880 880 outline: 0;
881 881 }
882 882
883 883 /* use same badge coloring in navbar inverse as in panel-heading */
884 884 .navbar-inverse {
885 885 .badge {
886 886 color: @navbar-inverse-bg;
887 887 background-color: @navbar-inverse-color;
888 888 }
889 889 }
890 890
891 891 /* pygments style */
892 892 div.search-code-body pre .match {
893 893 background-color: @highlight-color;
894 894 }
895 895 div.search-code-body pre .break {
896 896 background-color: @highlight-line-color;
897 897 width: 100%;
898 898 color: #747474;
899 899 display: block;
900 900 }
901 901 div.annotatediv {
902 902 margin-left: 2px;
903 903 margin-right: 4px;
904 904 }
905 905 .code-highlight {
906 906 border-left: 1px solid #ccc;
907 907 }
908 908 .code-highlight pre,
909 909 .linenodiv pre {
910 910 padding: 5px 2px 0px 5px;
911 911 margin: 0;
912 912 }
913 913 .code-highlight pre div:target {
914 914 background-color: #FFFFBE !important;
915 915 }
916 916 .linenos a { text-decoration: none; }
917 917
918 918 /* Stylesheets for the context bar */
919 919 #quick_login > .pull-right .list-group-item {
920 920 background-color: @kallithea-theme-main-color;
921 921 border: 0;
922 922 }
923 923 #content #context-pages .follow .show-following,
924 924 #content #context-pages .following .show-follow {
925 925 display: none;
926 926 }
927 927
928 928 nav.navbar #quick > li > a,
929 929 #context-pages > ul > li > a {
930 930 height: @navbar-height;
931 931 }
932
933 /* at.js */
934 .atwho-view strong {
935 /* the blue color doesn't look good, use normal color */
936 color: inherit;
937 }
@@ -1,141 +1,143 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3 <html xmlns="http://www.w3.org/1999/xhtml">
4 4 <head>
5 5 <title><%block name="title"/><%block name="branding_title"/></title>
6 6 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
7 7 <meta http-equiv="X-UA-Compatible" content="IE=10"/>
8 8 <meta name="robots" content="index, nofollow"/>
9 9 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10 10 <link rel="shortcut icon" href="${h.url('/images/favicon.ico')}" type="image/x-icon" />
11 11 <link rel="icon" type="image/png" href="${h.url('/images/favicon-32x32.png')}" sizes="32x32">
12 12 <link rel="icon" type="image/png" href="${h.url('/images/favicon-16x16.png')}" sizes="16x16">
13 13 <link rel="apple-touch-icon" sizes="180x180" href="${h.url('/images/apple-touch-icon.png')}">
14 14 <link rel="manifest" href="${h.url('/images/manifest.json')}">
15 15 <link rel="mask-icon" href="${h.url('/images/safari-pinned-tab.svg')}" color="#b1d579">
16 16 <meta name="msapplication-config" content="${h.url('/images/browserconfig.xml')}">
17 17 <meta name="theme-color" content="#ffffff">
18 18
19 19 ## CSS ###
20 20 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css', ver=c.kallithea_version)}" media="screen"/>
21 21 <%block name="css_extra"/>
22 22
23 23 ## JAVASCRIPT ##
24 24 <script type="text/javascript">
25 25 ## JS translations map
26 26 var TRANSLATION_MAP = {
27 27 'Cancel': ${h.jshtml(_("Cancel"))},
28 28 'Retry': ${h.jshtml(_("Retry"))},
29 29 'Submitting ...': ${h.jshtml(_("Submitting ..."))},
30 30 'Unable to post': ${h.jshtml(_("Unable to post"))},
31 31 'Add Another Comment': ${h.jshtml(_("Add Another Comment"))},
32 32 'Stop following this repository': ${h.jshtml(_('Stop following this repository'))},
33 33 'Start following this repository': ${h.jshtml(_('Start following this repository'))},
34 34 'Group': ${h.jshtml(_('Group'))},
35 35 'Loading ...': ${h.jshtml(_('Loading ...'))},
36 36 'loading ...': ${h.jshtml(_('loading ...'))},
37 37 'Search truncated': ${h.jshtml(_('Search truncated'))},
38 38 'No matching files': ${h.jshtml(_('No matching files'))},
39 39 'Open New Pull Request from {0}': ${h.jshtml(_('Open New Pull Request from {0}'))},
40 40 'Open New Pull Request for {0} &rarr; {1}': ${h.js(_('Open New Pull Request for {0} &rarr; {1}'))},
41 41 'Show Selected Changesets {0} &rarr; {1}': ${h.js(_('Show Selected Changesets {0} &rarr; {1}'))},
42 42 'Selection Link': ${h.jshtml(_('Selection Link'))},
43 43 'Collapse Diff': ${h.jshtml(_('Collapse Diff'))},
44 44 'Expand Diff': ${h.jshtml(_('Expand Diff'))},
45 45 'No revisions': ${h.jshtml(_('No revisions'))},
46 46 'Type name of user or member to grant permission': ${h.jshtml(_('Type name of user or member to grant permission'))},
47 47 'Failed to revoke permission': ${h.jshtml(_('Failed to revoke permission'))},
48 48 'Confirm to revoke permission for {0}: {1} ?': ${h.jshtml(_('Confirm to revoke permission for {0}: {1} ?'))},
49 49 'Enabled': ${h.jshtml(_('Enabled'))},
50 50 'Disabled': ${h.jshtml(_('Disabled'))},
51 51 'Select changeset': ${h.jshtml(_('Select changeset'))},
52 52 'Specify changeset': ${h.jshtml(_('Specify changeset'))},
53 53 'MSG_SORTASC': ${h.jshtml(_('Click to sort ascending'))},
54 54 'MSG_SORTDESC': ${h.jshtml(_('Click to sort descending'))},
55 55 'MSG_EMPTY': ${h.jshtml(_('No records found.'))},
56 56 'MSG_ERROR': ${h.jshtml(_('Data error.'))},
57 57 'MSG_LOADING': ${h.jshtml(_('Loading...'))}
58 58 };
59 59 var _TM = TRANSLATION_MAP;
60 60
61 61 var TOGGLE_FOLLOW_URL = ${h.js(h.url('toggle_following'))};
62 62
63 63 var REPO_NAME = "";
64 64 %if hasattr(c, 'repo_name'):
65 65 var REPO_NAME = ${h.js(c.repo_name)};
66 66 %endif
67 67
68 68 var _authentication_token = ${h.js(h.authentication_token())};
69 69 </script>
70 70 <script type="text/javascript" src="${h.url('/js/yui.2.9.js', ver=c.kallithea_version)}"></script>
71 71 <script type="text/javascript" src="${h.url('/js/jquery.min.js', ver=c.kallithea_version)}"></script>
72 72 <script type="text/javascript" src="${h.url('/js/jquery.dataTables.js', ver=c.kallithea_version)}"></script>
73 73 <script type="text/javascript" src="${h.url('/js/dataTables.bootstrap.js', ver=c.kallithea_version)}"></script>
74 74 <script type="text/javascript" src="${h.url('/js/bootstrap.js', ver=c.kallithea_version)}"></script>
75 75 <script type="text/javascript" src="${h.url('/js/select2.js', ver=c.kallithea_version)}"></script>
76 76 <script type="text/javascript" src="${h.url('/js/native.history.js', ver=c.kallithea_version)}"></script>
77 <script type="text/javascript" src="${h.url('/js/jquery.caret.min.js', ver=c.kallithea_version)}"></script>
78 <script type="text/javascript" src="${h.url('/js/jquery.atwho.min.js', ver=c.kallithea_version)}"></script>
77 79 <script type="text/javascript" src="${h.url('/js/base.js', ver=c.kallithea_version)}"></script>
78 80 ## EXTRA FOR JS
79 81 <%block name="js_extra"/>
80 82 <script type="text/javascript">
81 83 (function(window,undefined){
82 84 var History = window.History; // Note: We are using a capital H instead of a lower h
83 85 if ( !History.enabled ) {
84 86 // History.js is disabled for this browser.
85 87 // This is because we can optionally choose to support HTML4 browsers or not.
86 88 return false;
87 89 }
88 90 })(window);
89 91
90 92 $(document).ready(function(){
91 93 tooltip_activate();
92 94 show_more_event();
93 95 // routes registration
94 96 pyroutes.register('home', ${h.js(h.url('home'))}, []);
95 97 pyroutes.register('new_gist', ${h.js(h.url('new_gist'))}, []);
96 98 pyroutes.register('gists', ${h.js(h.url('gists'))}, []);
97 99 pyroutes.register('new_repo', ${h.js(h.url('new_repo'))}, []);
98 100
99 101 pyroutes.register('summary_home', ${h.js(h.url('summary_home', repo_name='%(repo_name)s'))}, ['repo_name']);
100 102 pyroutes.register('changelog_home', ${h.js(h.url('changelog_home', repo_name='%(repo_name)s'))}, ['repo_name']);
101 103 pyroutes.register('files_home', ${h.js(h.url('files_home', repo_name='%(repo_name)s',revision='%(revision)s',f_path='%(f_path)s'))}, ['repo_name', 'revision', 'f_path']);
102 104 pyroutes.register('edit_repo', ${h.js(h.url('edit_repo', repo_name='%(repo_name)s'))}, ['repo_name']);
103 105 pyroutes.register('edit_repo_perms', ${h.js(h.url('edit_repo_perms', repo_name='%(repo_name)s'))}, ['repo_name']);
104 106 pyroutes.register('pullrequest_home', ${h.js(h.url('pullrequest_home', repo_name='%(repo_name)s'))}, ['repo_name']);
105 107
106 108 pyroutes.register('toggle_following', ${h.js(h.url('toggle_following'))});
107 109 pyroutes.register('changeset_info', ${h.js(h.url('changeset_info', repo_name='%(repo_name)s', revision='%(revision)s'))}, ['repo_name', 'revision']);
108 110 pyroutes.register('changeset_home', ${h.js(h.url('changeset_home', repo_name='%(repo_name)s', revision='%(revision)s'))}, ['repo_name', 'revision']);
109 111 pyroutes.register('repo_size', ${h.js(h.url('repo_size', repo_name='%(repo_name)s'))}, ['repo_name']);
110 112 pyroutes.register('repo_refs_data', ${h.js(h.url('repo_refs_data', repo_name='%(repo_name)s'))}, ['repo_name']);
111 113 pyroutes.register('users_and_groups_data', ${h.js(h.url('users_and_groups_data'))}, []);
112 114 });
113 115 </script>
114 116
115 117 <%block name="head_extra"/>
116 118 </head>
117 119 <body>
118 120 <nav class="navbar navbar-inverse mainmenu">
119 121 <div class="navbar-header" id="logo">
120 122 <a class="navbar-brand" href="${h.url('home')}">
121 123 <span class="branding">${c.site_name}</span>
122 124 </a>
123 125 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
124 126 <span class="sr-only">Toggle navigation</span>
125 127 <span class="icon-bar"></span>
126 128 <span class="icon-bar"></span>
127 129 <span class="icon-bar"></span>
128 130 </button>
129 131 </div>
130 132 <div id="navbar" class="navbar-collapse collapse">
131 133 <%block name="header_menu"/>
132 134 </div>
133 135 </nav>
134 136
135 137 ${next.body()}
136 138
137 139 %if c.ga_code:
138 140 ${h.literal(c.ga_code)}
139 141 %endif
140 142 </body>
141 143 </html>
General Comments 0
You need to be logged in to leave comments. Login now