232 Commits
1.0.0 ... 1.1.0

Author SHA1 Message Date
rr-
6660ee77e1 Bumped version to 1.1.0
(Renamed 0.9.x to 1.0.x on GH)
2016-04-13 11:17:52 +02:00
7f4bebe404 Merge pull request #81 from kotcrab/arrows
Added arrow keys support
2016-04-06 08:10:47 +02:00
6a7792239e Add arrow keys support 2016-04-05 23:34:50 +02:00
rr-
f248a6ab4e Fixed user Markdown links 2016-03-13 15:54:00 +01:00
rr-
f12ce7a7c5 Fixed serving content w/o ranges caused errors 2016-03-10 14:36:41 +01:00
rr-
f8514bfdc7 Fix authorization test 2016-03-09 21:07:04 +01:00
01a256bbbc Merge pull request #79 from kotcrab/gruntfile-fix
Fix mousetrap library path
2016-03-09 20:22:37 +01:00
abae786748 Fix mousetrap library path 2016-03-09 20:12:04 +01:00
rr-
d3b794c9da Added MySQL to the list of dependencies 2016-03-08 23:24:13 +01:00
rr-
9d15bbfcce Updated the installation guide 2016-03-08 22:53:14 +01:00
rr-
525f05b570 Fixed npm bugging about unused fields 2016-03-08 22:11:16 +01:00
rr-
54d95c11c5 Corrected software dependencies in install guide 2016-03-08 22:10:11 +01:00
rr-
0a58e12827 Small OCD fixes to documentation 2016-03-08 21:59:18 +01:00
rr-
818d9ac3c8 Replaced SJIS border with background 2016-02-29 01:02:39 +01:00
rr-
9eaab55dab Increased maximum comment length to 5000 chars 2016-02-29 00:59:54 +01:00
rr-
36d2842b6e Added [sjis]...[/sjis] tag 2016-02-29 00:59:54 +01:00
rr-
87681f8c0d Added file_size search syntax 2016-02-19 09:54:10 +01:00
rr-
f49a8fabab Added image_{width,height,area} search order kinds 2016-02-19 09:51:37 +01:00
rr-
2bba57e8de Added image_{width,height,area} search syntax 2016-02-19 09:44:46 +01:00
rr-
f7df1cb536 Added image search for animations 2016-02-08 07:30:59 +01:00
rr-
2ab636e569 Added protection against unknown image sizes 2016-01-21 12:15:14 +01:00
rr-
21ddb8a90b Fixed post note link being shown for videos and YT 2016-01-13 21:25:12 +01:00
rr-
1ce16c80ec Fixed inability to add post notes to GIFs 2016-01-13 21:24:45 +01:00
rr-
0eabc4ed41 Added support for HTTP ranges (= .webm + seeking) 2015-12-30 20:08:16 +01:00
rr-
965f772515 Added special:tumbleweed 2015-12-29 12:24:35 +01:00
rr-
770dba8a41 Tweaked avatar margins in comment lists 2015-12-29 12:02:53 +01:00
rr-
0ea40ce6d0 Fixed CSS of lists in comment contents 2015-12-29 12:02:36 +01:00
rr-
e2bc5d3415 Added feature_count to search terms 2015-12-29 11:57:25 +01:00
rr-
5df5a78df5 Improved Markdown parsing of permalinks 2015-12-29 11:53:38 +01:00
rr-
13d01dee27 Changed youtube videos to 16:9 2015-12-27 17:05:48 +01:00
rr-
9fd34f06aa Limited post note width 2015-12-24 21:00:31 +01:00
rr-
92631df9a4 Fixed notes disappearing off the screen 2015-12-24 20:59:39 +01:00
rr-
e623513e3d Fixed SQL column ambiguity in filter parsing 2015-12-17 22:52:21 +01:00
rr-
7e2e90ad3f Removed hack for GIF preloading
It was effectively destroying caching.
2015-12-13 23:57:25 +01:00
rr-
7645c012a5 Added "upload:" alias as requested by John Doe 2015-12-13 21:48:53 +01:00
rr-
ecb3901bbe Fixed tests 2015-11-25 11:39:45 +01:00
rr-
ee09a09833 Fixed lazy loading users and posts for Favorite 2015-11-25 11:35:18 +01:00
rr-
13d77dd14a Fixed whitespace 2015-11-25 09:48:03 +01:00
rr-
b8f90dbd95 Fixed stats cron job 2015-11-25 08:13:25 +01:00
rr-
5305bb68a4 Simplified search parsing
Reduced execution flow dependencies and made all search parsers share
the basic code rather than implementing everything all over again in
each parser through awkward protected functions.
2015-11-25 01:25:43 +01:00
rr-
5aa75a4150 Fixed sorting users by registration time 2015-11-25 01:06:19 +01:00
rr-
b3c5212c84 Added ability to search tags by usage count 2015-11-24 21:57:14 +01:00
rr-
96195f0efc Added ability to search tags by creation/edit time 2015-11-24 21:57:14 +01:00
rr-
d769eaed61 Added ability to search posts by edit dates 2015-11-24 21:41:08 +01:00
rr-
2df43201ba Added order:edit_time support for tag list 2015-11-24 18:23:47 +01:00
rr-
40e869b848 Added support to search for posts by creation time 2015-11-24 18:14:11 +01:00
rr-
b7456463eb Commonized naming of "creation time" property
Rather than having "creation time", "upload time", "registration time"
etc. I think it is better to have a single "creation time"
entity-agnostic property (like the one Tags had thus far).
2015-11-24 18:14:11 +01:00
rr-
d49f76c9f1 Added ability to sort posts by feature count 2015-11-24 18:14:11 +01:00
rr-
645573a272 Changed global comment list sort to creation time
Before this change, it was using edit time, which resulted in bumping
old posts every time a comment was edited. Users reported this behavior
as awkward and unintuitive, so I've changed it.
2015-11-24 18:14:11 +01:00
rr-
f5aed19bf3 Added verbosity to image conversion errors 2015-11-23 11:35:09 +01:00
rr-
28bba097c3 Added ffmpeg support in Flash thumbnails creation 2015-11-01 11:02:50 +01:00
rr-
105a564c7d Fixed problem with %-encoded URIs 2015-09-30 20:03:32 +02:00
rr-
15739ac7cc Fixed adding ghost post notes after removing some 2015-09-16 07:43:18 +02:00
rr-
0edbd9bf40 Fixed editing new post notes duplicating them 2015-09-16 07:40:07 +02:00
rr-
a31d5849fc Improved connection error reporting 2015-09-09 22:32:18 +02:00
rr-
180252cc64 Removed masking of image extensions' exceptions 2015-09-06 22:57:53 +02:00
rr-
48bb4fc803 Added option to set image manipulation extension 2015-09-06 22:57:28 +02:00
rr-
58768acc1c Increased control over tag categories 2015-08-05 18:08:02 +02:00
rr-
42f37d8fee Further improved text of some error messages 2015-08-05 17:19:10 +02:00
rr-
ec5ff5f230 Increased margin for error messages 2015-08-05 16:57:33 +02:00
rr-
7350b89a33 Fixed mass tag
Regression from b3def7f.
2015-08-04 21:34:59 +02:00
rr-
91f33c9e08 Fixed pager not showing in recent edits
Regression from b3def7f.
2015-08-04 19:52:47 +02:00
rr-
6b933132a5 Added helpful messages for invalid search orders 2015-08-04 19:47:18 +02:00
rr-
8c87a93774 Added support for note count based post searches 2015-08-04 19:31:57 +02:00
rr-
7ca582186b Fixed redirection after tag editing
Regression from b3def7f.
2015-08-04 19:22:03 +02:00
rr-
465a61ff4a Improved API for post editing 2015-08-03 19:27:25 +02:00
rr-
1ad5d7475c Enabled restricted users to delete own accounts 2015-08-03 19:23:11 +02:00
rr-
ebd25cd9a9 Fixed removing users always logging out 2015-08-03 19:23:11 +02:00
rr-
b3def7fc21 Improved API responses 2015-08-03 19:23:11 +02:00
rr-
5a537ba168 Fixed "original width" fit on Webkit 2015-07-29 19:39:10 +02:00
rr-
b4db90bcdc Hidden most edit controls by default
This reduces form size in half, which should improve editing experience.
2015-07-19 19:32:09 +02:00
rr-
c6a17d33af Fixed fit modes not appearing in sidebar 2015-07-19 19:32:09 +02:00
rr-
37eabe1556 Improved post view on small layout
- post notes no longer disappear
- post image is larger
- sidebar stacks into columns
- things from sidebar are centered
2015-07-19 19:32:08 +02:00
rr-
1969f0e3fa Improved label naming in browsing settings 2015-07-19 19:32:06 +02:00
rr-
c0a474ed82 Added new fit mode for both dimensions 2015-07-19 19:00:53 +02:00
rr-
6380043a9a Added option to upscale small posts 2015-07-19 19:00:45 +02:00
rr-
b75df289e9 Fixed "fit to height" upscaling small posts 2015-07-19 18:05:16 +02:00
rr-
8db72633f6 Fixed broken post notes after fit mode changes 2015-07-19 18:03:47 +02:00
rr-
44ef66f65c Fixed fit mode state disappearing after AJAX calls 2015-07-19 12:35:55 +02:00
rr-
7511430b2a Fixed jshint warnings 2015-07-19 12:28:01 +02:00
rr-
362087ee63 Split cycle fit mode button to 1 for each fit mode 2015-07-19 12:26:11 +02:00
rr-
5ad854e38a Added fit mode to browsing settings 2015-07-19 11:53:34 +02:00
rr-
5882998c20 Added option to cycle fit mode to sidebar 2015-07-19 11:32:11 +02:00
rr-
579e59e7df Added fit to height mode to [F] hotkey 2015-07-19 11:29:14 +02:00
rr-
64ae9a7c74 Moved [F]ullscreen hotkey to PostContentPresenter 2015-07-19 10:58:50 +02:00
rr-
6b6acb0bbf Fixed appearance on luakit 2015-07-12 19:19:08 +02:00
rr-
11648e055c Added support for explicit HTTP permalinks 2015-07-02 20:24:01 +02:00
rr-
3c83f711c9 Removed MaxCDN dependency 2015-07-02 20:15:20 +02:00
rr-
bd7dd9a2ad Stripped file extensions from executables 2015-06-28 12:29:16 +02:00
rr-
02c8353175 Added shebangs and +x to scripts 2015-06-28 12:27:50 +02:00
rr-
77e51c2e10 Replaced some more whitespace 2015-06-28 12:26:10 +02:00
rr-
fd448bac87 Switched to PHPMailer 2015-06-28 12:24:46 +02:00
rr-
027b98ce76 Fixed dangling tags on MySQL after post removal 2015-06-28 10:24:40 +02:00
rr-
edee487ff9 Removed trailing newlines 2015-06-28 10:10:07 +02:00
rr-
2702518e31 Switched to spaces 2015-06-28 10:07:56 +02:00
rr-
79df9b56d3 Added FPM-based support for too big files 2015-06-28 09:59:47 +02:00
rr-
3b1544eff3 Fixed OOM errors in scripts 2015-06-27 19:03:47 +02:00
rr-
c74edbee51 Fixed overwriting redirection HTTP status codes 2015-06-27 18:35:21 +02:00
rr-
8407a3f70e Fixed getRequestHeaders for CGI servers 2015-06-27 18:07:31 +02:00
9c1db78b69 Fixed typo 2015-06-27 00:03:31 +02:00
9b2238d423 Fixed tagging uploaded posts in Chrome 2015-06-26 23:40:27 +02:00
b5d6e4837d Removed a way to repeat a tag in uploaded post 2015-06-24 23:51:11 +02:00
d20fe3d95a Fixed post labels taking unwanted focus 2015-06-05 09:55:25 +02:00
a7a2f31dc2 Fixed history navigation in comment list 2015-06-04 10:48:16 +02:00
b26fd88d6f Stripped www. from domain names 2015-06-03 22:49:46 +02:00
a69f8563e8 Fixed prev/next button behavior in pager 2015-05-29 16:51:15 +02:00
24d8bf5295 Improved search error messages 2015-05-23 10:46:17 +02:00
5412ac14b9 Added GIF detection 2015-05-23 10:05:05 +02:00
38bfbfb8f3 Added Tab and Shift+Tab support to autocomplete 2015-05-22 22:44:25 +02:00
4ba855871f Added invisible label for tag inputs
Improves integration with Vimperator
2015-05-16 12:36:43 +02:00
0727433a9e Wrapped thumbnails with links in post upload form
Improves integration with Vimperator
2015-05-16 12:31:27 +02:00
627a8db5f3 Added prev/next buttons in post upload form 2015-05-16 12:31:14 +02:00
740cc85775 Fixed jshint warning 2015-05-16 12:30:48 +02:00
48004f1117 Added prev/next page buttons for pager
Integrated better with vimperator.
2015-05-14 23:29:42 +02:00
4126de8e25 Added blurring of active element on post edit
Integrates better with vimperator.
2015-05-14 23:13:55 +02:00
c569504ce7 Added ability to turn keyboard shortcuts off 2015-05-14 23:04:30 +02:00
8d119d2b62 Fixed upload messages margin on screen screens 2015-05-09 19:32:04 +02:00
f8851bf26d Fixed jshint warnings 2015-05-08 18:39:45 +02:00
19e7fa94f7 Fixed autocomplete position near page bottom 2015-05-08 18:39:20 +02:00
06180f5b50 Fixed showing preview link for non-images in upload 2015-03-21 08:17:58 +01:00
aa228d5125 Further improved memory footprint in post upload 2015-03-21 08:16:02 +01:00
5f4260d0a7 Fixed uploading posts from URLs 2015-03-20 13:35:10 +01:00
e7e50cfb3a Fixed one-letter hotkeys not firing in radioboxes 2015-03-19 23:02:53 +01:00
09d8e5ae1c Fixed prev/next post selectors moving page 2015-03-19 22:57:37 +01:00
fce9c3483a Added link to full image preview in post uploads 2015-03-19 22:49:24 +01:00
a3157a48ec Removed lightbox from post uploads 2015-03-19 22:49:24 +01:00
c35ed15946 Increased preview size in uploads 2015-03-19 22:49:19 +01:00
f75b4505a1 Reduced memory footprint for long upload sessions 2015-03-19 22:07:43 +01:00
d98474cc6a Fixed broken home page for anonymous users 2015-03-14 23:07:34 +01:00
2e06422b62 Shortened GIF guard 2015-03-13 21:16:34 +01:00
0aad36228a Added featured post uploader name to home page 2015-03-13 21:00:09 +01:00
7c77c7a87b Fixed post resizing, alignment etc. 2015-03-13 20:56:12 +01:00
0cf29a657a Fixed Upgrade35 wiping tag and post relations 2015-03-13 09:47:30 +01:00
fdb029eb5c Increased post notes placement precision 2015-03-13 09:47:07 +01:00
eb3b02c28d Improved post sizing, added [F]ullscreen hotkey 2015-03-13 09:38:50 +01:00
65bc6705d3 Added script for finding dead posts 2015-03-11 10:15:01 +01:00
5f0706c0b4 Fixed orphan records and denormalization errors 2015-02-23 20:42:12 +01:00
e7ea60f293 Fixed bad arrows behavior while editing post notes 2015-02-22 19:30:07 +01:00
b416868aa7 Added arrow hotkeys to Draggable and Resizable
Which means better control over the post notes placement for all the
keyboard lovers.
2015-02-22 18:56:35 +01:00
90406b1278 Refactored Resizable to match Draggable 2015-02-22 18:47:32 +01:00
04a16a2a36 Added delete key support for auto complete 2015-02-22 18:43:35 +01:00
72e9400e1d Fixed upload button margin 2015-02-22 12:11:20 +01:00
d425b0df2e Fixed inconsistent margins 2015-02-22 10:32:55 +01:00
4cad09b85e Added space after tags in tag input 2015-02-14 14:00:16 +01:00
a59a57fe70 Increased limit for pasted text length 2015-02-13 08:35:22 +01:00
0c4d984157 Reduced font size by 2px 2015-01-27 09:22:41 +01:00
ea5262fa2b Fixed font size for form elements 2015-01-26 22:15:12 +01:00
2ab4da11fc Improved font scaling on Android 2015-01-26 08:50:25 +01:00
9090ac6fb9 Fixed broken test 2014-12-26 09:49:35 +01:00
eb77b6811a Fixed negative order in searches 2014-12-26 09:48:04 +01:00
0945ed64ee Added logging of exceptions thrown by templates
(finally)
2014-12-20 12:54:39 +01:00
4d9fc51819 Fixed global comment list 2014-12-20 12:51:22 +01:00
1897297127 Added search query minifying
Seeing 'page=1' and 'query=' in every other link was tiresome. I changed
the rules so that such keys are appended only if they hold nontrivial
values.
2014-12-20 10:36:29 +01:00
970b9bf06d Simplified util/misc.js requires 2014-12-20 10:30:10 +01:00
e5f2e293f0 Bumped version to 0.9.2 2014-12-14 21:00:38 +01:00
43334b33e1 Fixed suggestions not hiding in the upload 2014-12-14 20:49:42 +01:00
839e97b1e2 Fixed history for posts uploaded anonymously
Until now posts uploaded anonymously were not so anonymous - the author
was clearly visible in post history (and tag history if the upload has
led to creation of any new tags). Both of these are fixed now.
2014-12-14 09:30:22 +01:00
8a33b9581d Tweaked font size for <small/> tag 2014-12-07 13:43:15 +01:00
7067d8e13d Added support for [small]text[/small] 2014-12-07 13:43:04 +01:00
5769034223 Fixed tag input behavior for initial text 2014-11-30 20:30:06 +01:00
c0a5c800e0 Fixed autocomplete staying even after hiding input 2014-11-30 13:05:31 +01:00
b70231bd7e Changed used tags in autocomplete to be grayed-out 2014-11-30 12:49:48 +01:00
bc757dd883 Fixed reused implied tags marked as duplicates 2014-11-30 12:49:48 +01:00
924592675c Fixed tag suggestions for implied tags 2014-11-30 12:40:22 +01:00
a3aea27a13 Fixed tag edits not triggering tag list updates 2014-11-30 12:40:22 +01:00
3c54671aeb Widened post edit form 2014-11-30 11:58:39 +01:00
d8df51f0c0 Moved suggestions before siblings 2014-11-30 11:55:24 +01:00
b693a5f4b3 Added tag siblings suggestion synchronization 2014-11-30 11:54:38 +01:00
303f91e15c Refactored tag suggestions 2014-11-30 11:45:49 +01:00
c350c47195 Added showing tag suggestions on click 2014-11-30 11:31:40 +01:00
24ce67b4ff Added auto completion to tag list presenter 2014-11-30 00:22:50 +01:00
06d7c19556 Added POST to tag merging API 2014-11-28 23:18:26 +01:00
728d1d65de Fixed mass tag 2014-11-28 23:18:23 +01:00
4b27b8a85d Fixed updating user settings 2014-11-28 23:18:14 +01:00
bfe31d87a1 Fixed history presenter 2014-11-27 18:22:48 +01:00
2fd371b10a Added absolute timestamp hints where necessary 2014-11-27 10:34:45 +01:00
f1647a5f7b Fixed precision loss in post disk space usage 2014-11-22 18:08:34 +01:00
cd688b25a3 Fixed example tag usages not showing up 2014-11-22 17:54:37 +01:00
a7c6e9f043 Fixed putting "null" in post sources 2014-11-22 17:49:35 +01:00
997c2a10ec Renamed SearchServices directory to Search 2014-11-22 14:56:30 +01:00
95cf0ca37b Moved Controllers/ViewProxies to ViewProxies 2014-11-22 14:53:40 +01:00
116522498d Removed obsolete method in InputReader 2014-11-22 14:47:25 +01:00
01a84ee4e2 Changed account settings editing
Account settings editing no longer encapsulates file content in base64.
2014-11-22 14:45:48 +01:00
2458935fdf Changed post editing
Post editing no longer encapsulates file content in base64.
2014-11-22 14:45:45 +01:00
f2b1e3bedb Changed post uploads
Post uploads no longer encapsulates file content in base64.
This means dramatic speed up for sending on local networks.
2014-11-22 14:37:00 +01:00
77c51d9a8a Added support for FormData in JS API facade 2014-11-22 14:31:53 +01:00
d8d65ed24c Changed PUT requests to POST
HTTP spec disallows multipart/form-data requests using PUT method.
2014-11-22 14:30:29 +01:00
58d3129548 Removed dangling console.log 2014-11-22 14:04:40 +01:00
0b032e7f94 Merge branch 'api' 2014-11-22 13:10:24 +01:00
4e6fe634e1 Fixed post list disregarding viewPosts privilege 2014-11-22 13:10:00 +01:00
b14f02810e Fixed everyone could view every post 2014-11-22 13:00:36 +01:00
796c2d1b1f Fixed everyone could feature posts 2014-11-22 12:58:02 +01:00
40197d6c39 Fixed everyone could delete posts 2014-11-22 12:57:52 +01:00
736c0a66ff Fixed comments disregarding viewUsers privilege 2014-11-22 12:56:44 +01:00
48230a64ad Simplified routing 2014-11-22 12:44:45 +01:00
da6b37b14c Merge branch 'master' into api 2014-11-21 22:34:04 +01:00
4b4ccf365a Fixed entity IDs being strings
This coincidentally fixes editing newly added comments.
2014-11-21 22:32:10 +01:00
2b0a4d1f76 Removed AJAX external caching
jQuery's caching caused problems in some scenarios, for example with
scoring/faving posts.
2014-11-21 22:24:06 +01:00
2d1b5308f3 Removed unneeded dependencies 2014-11-21 22:16:56 +01:00
8fb1b87ae5 Fixed post editing 2014-11-21 22:16:56 +01:00
9621810332 Fixed syntax error 2014-11-21 22:16:56 +01:00
bd33b09f7b Fixed private methods 2014-11-21 22:16:31 +01:00
3245c75187 Removed controller layer 2014-11-21 22:16:31 +01:00
e38152b921 Moved user avatar controller to routes 2014-11-21 22:16:31 +01:00
2195b2c9a1 Moved scores controller to routes 2014-11-21 22:16:31 +01:00
969f70318b Moved user controller to routes 2014-11-21 22:16:31 +01:00
06cc776438 Removed .swp files... 2014-11-21 22:05:53 +01:00
76edbfeddb Moved tag controller to routes 2014-11-21 15:49:53 +01:00
9de6e7e739 Moved post notes controller to routes 2014-11-21 12:54:33 +01:00
d8a4e1ec4e Moved post controller to routes 2014-11-21 12:45:47 +01:00
193d1c5f7a Moved post content controller to routes 2014-11-21 11:27:51 +01:00
7ff961fc21 Moved history controller to routes 2014-11-21 10:33:15 +01:00
a11436aa8c Added more tests to route repository test 2014-11-21 10:30:53 +01:00
a3b02adb7f Moved global param controller to routes 2014-11-21 10:30:52 +01:00
40b16f586b Fixed RouteRepositoryTest using real DB 2014-11-21 10:30:51 +01:00
602d7a1f45 Split favorites controller to routes 2014-11-21 10:23:50 +01:00
333c538f1e Split comment controller to routes 2014-11-21 10:23:49 +01:00
7c182f57a0 Added support for arguments in new Routes 2014-11-21 10:23:49 +01:00
2a7ca79b2d Added Route layer proof of concept 2014-11-21 10:23:49 +01:00
9e894bc41c Fixed ControllerRepositoryTest using real DB 2014-11-21 10:21:26 +01:00
cdd2726f30 Improved MySQL integration 2014-11-21 10:20:04 +01:00
029f0f00a4 Improved test database upgrade script 2014-11-21 10:03:08 +01:00
c6fe7a4320 Changed downloading YouTube posts in backend
Trying to download YouTube posts by visiting backend directly results in
a redirect to YouTube instead of showing an ugly error.
2014-11-19 19:47:54 +01:00
8bd4ae27c2 Fixed bad message when serving non-existing files 2014-11-19 19:47:54 +01:00
2a215ef51b Removed download link for YouTube posts 2014-11-19 19:47:50 +01:00
6473ed74d3 Fixed post note removal didn't generate snapshot 2014-11-19 19:40:06 +01:00
44a4184eb8 Changed snapshot merging to work for deletions
Creating and deleting stuff will remove history snapshots if it occurs
within five minute gap. This is to prevent spamming history with
tag names that are introduced by an accident and removed shortly after.
2014-11-19 19:37:10 +01:00
847f248829 Added snapshot compression 2014-11-19 10:22:59 +01:00
a0133ea632 Added snapshot merging 2014-11-18 21:22:46 +01:00
0b15ca1b05 Fixed current search not showing arrow 2014-11-18 16:06:12 +01:00
2996f27671 Fixed scoremin and scoremax 2014-11-17 23:04:23 +01:00
6a751ed0b2 Fixed user list presenter disrespecting privileges
If user had no right to view user accounts, the list presenter ignored
that and linked to pages that shown privilege errors. Now it shows the
links only if user has right to view user accounts.
2014-11-17 22:16:30 +01:00
437 changed files with 23389 additions and 20390 deletions

View File

@ -1,28 +1,40 @@
Prerequisites
-------------
In order to run szurubooru, you need to have installed following software:
In order to run `szurubooru`, you need to have installed following software:
- Apache2
- mod_rewrite
- mod_mime_magic (recommended)
- PHP 5.6.0
- pdo_sqlite
- imagick or gd
- composer (PHP package manager)
- npm (node.js package manager)
- `Apache` 2.4+
- `mod_rewrite`
- `mod_mime_magic` (recommended)
- `PHP` 5.6.0+
- `pdo_mysql`
- `imagick` or `gd`
- `MySQL` or `MariaDB`
- `composer` (`PHP` package manager)
- `npm` (`node.js` package manager)
Optional modules:
Optional software:
- dump-gnash or swfrender for flash thumbnails
- ffmpegthumbnailer or ffmpeg for video thumbnails
- `dump-gnash`, `swfrender` or `ffmpeg` for Flash thumbnails
- `ffmpegthumbnailer` or `ffmpeg` for video thumbnails
Cloning the repository
----------------------
Download the repository somewhere you will it run from, or better yet, clone it
with `git`:
cd /srv/www/
git clone https://github.com/rr-/szurubooru booru-test
Fetching dependencies
---------------------
To fetch dependencies that szurubooru needs in order to run, enter following
To fetch dependencies that `szurubooru` needs in order to run, enter following
commands in the terminal:
composer update
@ -30,11 +42,11 @@ commands in the terminal:
Running grunt tasks
-------------------
Running `grunt` tasks
---------------------
Szurubooru uses grunt to run tasks like database ugprades and tests. In order
to use grunt from the terminal, you can use:
`szurubooru` uses `grunt` to run tasks like database upgrades and tests. In
order to use `grunt` from the terminal, you can use:
node_modules/grunt-cli/bin/grunt [TASK]
@ -43,25 +55,25 @@ administrator:
npm install -g grunt-cli
This will add "grunt" to your PATH, making things much more human-friendly.
This will add `grunt` to your PATH, making things much more human-friendly.
grunt [TASK]
Enabling required modules in PHP
--------------------------------
Enabling required modules in `PHP`
----------------------------------
Enable required modules in php.ini (or other configuration file, depending on
Enable required modules in `php.ini` (or other configuration file, depending on
your setup):
;Linux
extension=pdo_sqlite.so
extension=pdo_mysql.so
;Windows
extension=php_pdo_sqlite.dll
extension=php_pdo_mysql.dll
In order to draw thumbnails, szurubooru needs either imagick or gd2:
In order to draw thumbnails, `szurubooru` needs either `Imagick` or `gd2`:
;Linux
extension=imagick.so
@ -73,25 +85,16 @@ In order to draw thumbnails, szurubooru needs either imagick or gd2:
Upgrading the database
----------------------
Every time database schema changes, you should upgrade the database by running
following grunt task in the terminal:
grunt upgrade
Creating virtual server in Apache
---------------------------------
In order to make Szurubooru visible in your browser, you need to create a
virtual server. This guide focuses on Apache2 web server. Note that although it
should be also possible to host szurubooru with nginx, you'd need to manually
translate the rules inside public_html/.htaccess into nginx configuration.
In order to make `szurubooru` visible in your browser, you need to create a
virtual server. This guide focuses on `Apache` web server. Note that although
it should be also possible to host `szurubooru` with `nginx`, you'd need to
manually translate the rules inside `public_html/.htaccess` into `nginx`
configuration.
Creating virtual server for Apache comes with no surprises, basically all you
Creating virtual server for `Apache` comes with no surprises, basically all you
need is the most basic configuration:
<VirtualHost *:80>
@ -99,30 +102,34 @@ need is the most basic configuration:
DocumentRoot /path/to/szurubooru/public_html/
</VirtualHost>
ServerName specifies the domain under which szurubooru will be hosted.
DocumentRoot should point to the public_html/ directory.
`ServerName` specifies the domain under which `szurubooru` will be hosted.
`DocumentRoot` should point to the `public_html/` directory.
Some environments / configurations require extra steps to make things work - in
case you experience any problems, please consult the troubleshooting section
later in this file.
Enabling required modules in Apache
-----------------------------------
Enable required modules in httpd.conf (or other configuration file, depending
Enable required modules in `httpd.conf` (or other configuration file, depending
on your setup):
LoadModule rewrite_module mod_rewrite.so ;Linux
LoadModule rewrite_module modules/mod_rewrite.so ;Windows
Enable PHP support:
Enable `PHP` support:
LoadModule php5_module /usr/lib/apache2/modules/libphp5.so ;Linux
LoadModule php5_module /path/to/php/php5apache2_4.dll ;Windows
AddType application/x-httpd-php .php
PHPIniDir /path/to/php/
Enable MIME auto-detection (not required, but recommended - szurubooru doesn't
use file extensions, and reporting correct Content-Type to browser is always a
good thing):
Enable MIME auto-detection (not required, but recommended - `szurubooru`
doesn't use file extensions, and reporting correct `Content-Type` to browser is
always a good thing):
;Linux
LoadModule mime_magic_module mod_mime_magic.so
@ -137,21 +144,40 @@ good thing):
</IfModule>
Creating administrator account
------------------------------
By now, you should be able to view szurubooru in the browser. Registering
administrator account is simple - the first user to create an account
automatically becomes administrator and doesn't need e-mail activation.
Overwriting configuration
-------------------------
Everything that can be configured is stored in data/config.ini file. In order
to make changes there, copy the file and name it local.ini. Make sure you don't
edit the file itself, especially if you want to contribute.
Everything that can be configured is stored in `data/config.ini` file. In order
to make changes there, copy the file and name it `local.ini` and place it in
`data/` directory as well. Make sure you don't edit the `data/config.ini` file
itself, especially if you want to contribute.
Setting up the database
-----------------------
Before running `szurubooru` for first time, you need to set up the database.
`szurubooru` uses MySQL, so let's fire `mysql` and type following:
create user 'maria' identified by 'arkadia';
create database booru_test;
grant all privileges on *.* to 'maria'@'%' with grant option;
Then you need to provide the above credentials in the configuration files as
described in the previous section. Example `local.ini` file:
[database]
dsn = mysql:dbname=booru_test
user = maria
password = arkadia
After that, upgrade the database using following command:
grunt upgrade
This should be also executed every time database schema changes.
@ -165,21 +191,36 @@ smallest possible packages, run following command:
grunt build
This should create public_html/app.min.js, public_html/app.min.css and
public_html/app.min.html. .htaccess is configured so that if these files exist,
it will load them instead of development environment. To delete these
This should create `public_html/app.min.js`, `public_html/app.min.css` and
`public_html/app.min.html`. `.htaccess` is configured so that if these files
exist, it will load them instead of development environment. To delete these
conveniently, you can run:
grunt clean
If, for any reason, you do not wish to minify the resources, you should at
least copy the dependencies fetched before to the `public_html/` directory with
following:
grunt copy
Creating administrator account
------------------------------
By now, you should be able to view `szurubooru` in the browser. Registering
administrator account is simple - the first user to create an account
automatically becomes administrator and doesn't need e-mail activation.
Troubleshooting
---------------
1. Problems with Apache virtual servers
1. Problems with `Apache` virtual servers
After reloading Apache configuration, if you find yourself unable to
After reloading `Apache` configuration, if you find yourself unable to
connect to the server, make sure that connections are open, for example,
like this:
@ -187,40 +228,28 @@ Troubleshooting
Require all granted
</Directory>
(Note that Apache versions prior to 2.4 used "Allow from all" directive.)
(Note that `Apache` versions prior to 2.4 used `Allow from all` directive.)
Additionally, in order to access virtual host from your machine, make sure
the domain name "example.com" supplied in <VirtualHost/> section is
included in your hosts file (usually /etc/hosts on Linux and
C:/windows/system32/drivers/etc/hosts in Windows).
Additionally, in order to access the virtual host from your machine, make
sure the domain name `example.com` supplied in `<VirtualHost/>` section is
included in your `hosts` file (usually `/etc/hosts` on Linux and
`C:/windows/system32/drivers/etc/hosts` on Windows).
If the site doesn't work for you, make sure Apache can parse .htaccess
files. If it can't, you need to set AllowOverride option to "yes", for
example by putting following snippet inside <VirtualHost/> section:
If the site doesn't work for you, make sure `Apache` can parse `.htaccess`
files. If it can't, you need to set `AllowOverride` option to `yes`, for
example by putting following snippet inside the `<VirtualHost/>` section:
<Directory /path/to/szurubooru/public_html/>
AllowOverride All
</Directory>
2. Problems with PHP modules or registration
2. Problems with `PHP` modules or registration
Make sure your php.ini path is correct. Make sure all the modules are
actually loaded by inspecting phpinfo - create small file containing:
Make sure your `php.ini` path is correct. Make sure all the modules are
actually loaded by inspecting results of `phpinfo()` call - create small
file containing:
<?php phpinfo(); ?>
Then, run it in your browser and inspect the output, looking for missing
modules that were supposed to be loaded.
3. "Attempt to write to read-only database"
Make sure Apache has permission to access the database file AND directory
it's stored in. (SQLite writes temporary journal files to the parent
database directory). If you're the only user of the system, you can run
these commands without worrying too much:
chmod 0777 data/
chmod 0777 data/db.sqlite
Otherwise, if you're feeling fancy, you can experiment with setfacl on
Linux or group policies on Windows.

View File

@ -5,19 +5,21 @@ szurubooru
## What is it?
Szurubooru is a Danbooru-style board, a gallery where users can upload, browse,
tag and comment images, video clips and flash animations.
`szurubooru` is a Danbooru-style board, a gallery where users can upload,
browse, tag and comment images, video clips and flash animations.
Its name have its roots in Polish language and has onomatopoeic meaning of
scraping or scrubbing. It is pronounced *"shoorubooru"* [ˌʃuruˈburu].
## Licensing
Please see the file named [`LICENSE`](https://github.com/rr-/szurubooru/blob/master/LICENSE).
Please see the file named
[`LICENSE`](https://github.com/rr-/szurubooru/blob/master/LICENSE).
## Installation
Please see the file named [`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
Please see the file named
[`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
## Bugs and feature requests
@ -29,8 +31,8 @@ please do following:
to your problem, comment on that issue instead of opening a new one.
2. If you found an issue and the issue is closed, feel free to reopen it.
3. If you're reporting a bug, create an isolated and reproducible scenario.
4. If you're filing a feature request, provide examples - what might be obvious
to you, might not be so obvious to the developers.
4. If you're filing a feature request, provide examples - what might be
obvious to you, might not be so obvious to the developers.
## Contributing the code
@ -40,22 +42,23 @@ Here are some guidelines on how to contribute:
- Respect coding standards - be consistent with existing code base.
- Watch your whitespace - don't leave any characters at the end of the lines.
- Always run tests before pushing.
- Before starting, see [`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
- Before starting, see
[`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
- Use `grunt` to do automatic tasks like minifying Javascript files or running
tests. Run `grunt --help` to see full list of available tasks.
## API
Szurubooru from version 0.9+ uses REST API. Currently there is no formal
`szurubooru` from version 0.9+ uses REST API. Currently there is no formal
documentation; source code behind REST layer lies in `src/Controllers/`
directory. In order to use the API, bear in mind that you need to:
1. Have actual user account on the server to do most things (depending on
privileges).
2. Authenticate your requests:
1. Send user credentials to `/auth`. You'll receive authentication token in
1. Send user credentials to `/auth`. You'll receive authentication token in
return.
2. Send this token in X-Authorization-Token header on subsequent requests.
2. Send this token in X-Authorization-Token header on subsequent requests.
Developers reserve right to change API at any time with neither prior notice
nor keeping backwards compatibility.

View File

@ -1,6 +1,7 @@
{
"require": {
"mnapoli/php-di": "~4.4"
"mnapoli/php-di": "~4.4",
"phpmailer/phpmailer": "~5.2"
},
"require-dev": {

View File

@ -3,8 +3,12 @@ serviceName = szurubooru
serviceBaseUrl = http://localhost/
[mail]
botName = szurubooru bot
botAddress = noreply@localhost
smtpHost = localhost
smtpPort = 25
smtpUserName = bot
smtpUserPass = groovy123
smtpFrom = noreply@szurubooru
smtpFromName = szurubooru bot
passwordResetSubject = szurubooru - password reset
passwordResetBodyPath = mail/password-reset.txt
activationSubject = szurubooru - account activation
@ -19,7 +23,7 @@ maxCustomThumbnailSize = 1048576 ;1mb
[database.tests]
dsn = mysql:host=localhost
user = szuru_test
user = szuru-test
password = cat
[security]
@ -27,12 +31,13 @@ secret = change
minPasswordLength = 5
needEmailActivationToRegister = 1
defaultAccessRank = restrictedUser
forceHttpInPermalinks = 0
[security.privileges]
register = anonymous
listUsers = regularUser, powerUser, moderator, administrator
viewUsers = regularUser, powerUser, moderator, administrator
deleteOwnAccount = regularUser, powerUser, moderator, administrator
deleteOwnAccount = restrictedUser, regularUser, powerUser, moderator, administrator
deleteAllAccounts = administrator
changeOwnName = regularUser, powerUser, moderator, administrator
changeOwnAvatarStyle = regularUser, powerUser, moderator, administrator
@ -94,12 +99,13 @@ usersPerPage = 20
postsPerPage = 40
[tags]
categories[] = meta
categories[] = artist
categories[] = character
categories[] = copyright
categories[] = 'meta, meta, #aaa'
categories[] = 'artist, artist, #a00'
categories[] = 'character, character, #0a0'
categories[] = 'copyright, copyright, #a0a'
[misc]
thumbnailCropStyle = outside
customFaviconUrl = /favicon.png
dumpSqlIntoQueries = 0
imageExtension = imagick

View File

@ -81,12 +81,13 @@ module.exports = function(grunt) {
files: [
{ src: 'node_modules/jquery/dist/jquery.min.js', dest: 'public_html/lib/jquery.min.js' },
{ src: 'node_modules/jquery.cookie/jquery.cookie.js', dest: 'public_html/lib/jquery.cookie.js' },
{ src: 'node_modules/Mousetrap/mousetrap.min.js', dest: 'public_html/lib/mousetrap.min.js' },
{ src: 'node_modules/mousetrap/mousetrap.min.js', dest: 'public_html/lib/mousetrap.min.js' },
{ src: 'node_modules/pathjs/path.js', dest: 'public_html/lib/path.js' },
{ src: 'node_modules/underscore/underscore-min.js', dest: 'public_html/lib/underscore.min.js' },
{ src: 'node_modules/marked/lib/marked.js', dest: 'public_html/lib/marked.js' },
{ src: 'node_modules/nprogress/nprogress.js', dest: 'public_html/lib/nprogress.js' },
{ src: 'node_modules/nprogress/nprogress.css', dest: 'public_html/lib/nprogress.css' },
{ cwd: 'node_modules', src: 'font-awesome/**/*', dest: 'public_html/lib/', expand: true },
]
}
},
@ -136,7 +137,7 @@ module.exports = function(grunt) {
templates: readTemplates(grunt),
timestamp: grunt.template.today('isoDateTime'),
maxPostSize: config.database.maxPostSize,
tagCategories: config.tags.categories,
tagCategories: config.tags.categories.map(function(s) { return s.split(/,\s*/); }),
}
},
dist: {
@ -162,7 +163,7 @@ module.exports = function(grunt) {
});
grunt.registerTask('update', 'Upgrade database to newest version.', function() {
exec('php scripts/upgrade.php');
exec('php scripts/upgrade');
});
grunt.registerTask('upgrade', ['update']);

View File

@ -1,24 +1,25 @@
{
"name": "szurubooru",
"version": "0.9.0",
"dependencies": {
"jquery.cookie": "1.4.1",
"jquery": "~2.1.1",
"underscore": "1.7.0",
"Mousetrap": "git://github.com/ccampbell/mousetrap.git",
"marked": "~0.3.2",
"nprogress": "git://github.com/rstacruz/nprogress.git",
"requirejs": "*",
"ini": "*",
"grunt": "~0.4.5",
"grunt-processhtml": "*",
"grunt-contrib-uglify": "*",
"grunt-contrib-cssmin": "*",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-copy": "*",
"grunt-cli": "*",
"shelljs": "~0.3.0",
"rimraf": "~2.1"
}
"name": "szurubooru",
"version": "1.0.3",
"private": true,
"dependencies": {
"Mousetrap": "git://github.com/ccampbell/mousetrap.git",
"font-awesome": "^4.3.0",
"grunt": "~0.4.5",
"grunt-cli": "*",
"grunt-contrib-copy": "*",
"grunt-contrib-cssmin": "*",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-uglify": "*",
"grunt-processhtml": "*",
"ini": "*",
"jquery": "~2.1.1",
"jquery.cookie": "1.4.1",
"marked": "~0.3.2",
"nprogress": "git://github.com/rstacruz/nprogress.git",
"requirejs": "*",
"rimraf": "~2.1",
"shelljs": "~0.3.0",
"underscore": "1.7.0"
}
}

View File

@ -145,12 +145,6 @@
<!-- Indentation -->
<!-- **************** -->
<!-- Tests to make sure that a line does not contain the tab character. -->
<test name="indentation"> <!-- noTabs -->
<property name="type" value="tabs"/> <!-- tabs or spaces -->
<property name="number" value="4"/> <!-- number of spaces if type = spaces -->
</test>
<!-- Check the position of the open curly brace in a control structure (if) -->
<!-- sl = same line -->
<!-- nl = new line -->

View File

@ -4,6 +4,9 @@ DirectoryIndex index.html
ErrorDocument 404 /404.html
RewriteEngine On
RewriteBase /
RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
RewriteRule ^(.*)$ http://%1/$1 [R=301,L]
RewriteRule ^/?404.html$ /#/404 [NE,R,L]

View File

@ -1,105 +1,128 @@
.comment-form {
margin: 1em 0 2em;
margin: 1em 0 2em;
}
.comment-form .preview {
background: lemonchiffon;
padding: 0.5em;
margin-bottom: 1em;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5) inset;
display: none;
background: lemonchiffon;
padding: 0.5em;
margin-bottom: 1em;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5) inset;
display: none;
}
.comments ul {
list-style-type: none;
margin: 1em 0;
padding: 0;
ul.comments {
list-style-type: none;
margin: 1em 0;
padding: 0;
}
.comment ul {
list-style-position: inside;
margin: 1em 0;
padding: 0;
}
.comment {
margin: 0 0 1em 0;
padding: 0;
display: flex;
margin: 0 0 1em 0;
padding: 0;
display: -webkit-flex;
display: flex;
}
.comment .avatar {
margin-right: 0.5em;
flex-shrink: 0;
vertical-align: top;
margin-top: 0.2em;
margin-right: 0.75em;
-webkit-flex-shrink: 0;
flex-shrink: 0;
vertical-align: top;
}
.comment .content {
margin-top: 0.25em;
}
.comment .content p:first-child {
margin-top: 0;
margin-top: 0;
}
.comment .header {
line-height: 16pt;
vertical-align: middle;
line-height: 16pt;
vertical-align: middle;
}
.comment .score,
.comment .date {
color: silver;
font-size: 90%;
padding-left: 0.5em;
color: silver;
font-size: 90%;
padding-left: 0.5em;
}
.comment .score-up.active,
.comment .score-down.active {
font-weight: bold;
font-weight: bold;
}
.comment .header .ops a {
color: silver;
font-size: 80%;
color: silver;
font-size: 80%;
}
.comment .header .ops a:first-of-type:before {
margin-left: 0.5em;
content: '[';
margin-left: 0.5em;
content: '[';
}
.comment .header .ops a:not(:first-of-type):before {
content: '|';
margin: 0 0.3em;
content: '|';
margin: 0 0.3em;
}
.comment .header .ops a:last-of-type:after {
content: ']';
content: ']';
}
#global-comment-list {
text-align: center;
text-align: center;
}
#global-comment-list .pagination-content {
text-align: left;
text-align: left;
}
#global-comment-list .comments>ul {
margin-top: 0;
margin-top: 0;
}
#global-comment-list ul.posts {
list-style-type: none;
margin: 0;
padding: 0;
list-style-type: none;
margin: 0;
padding: 0;
}
#global-comment-list ul.posts>li {
margin-bottom: 2em;
margin-bottom: 2em;
}
#global-comment-list .post-comment {
display: flex;
display: -webkit-flex;
display: flex;
}
@media all and (max-width: 40em) {
#global-comment-list .post-comment {
flex-direction: column;
}
#global-comment-list .post-comment {
-webkit-flex-direction: column;
flex-direction: column;
}
}
#global-comment-list .post {
flex-shrink: 0;
flex-grow: 0;
margin-right: 1em;
margin-bottom: 1em;
-webkit-flex-shrink: 0;
-webkit-flex-grow: 0;
flex-shrink: 0;
flex-grow: 0;
margin-right: 1em;
margin-bottom: 1em;
}
#global-comment-list .comment-add,
#global-comment-list .comments>h1 {
display: none;
display: none;
}
#global-comment-list .post-small a {
margin: 0;
#global-comment-list .post-small .link {
margin: 0;
}
.sjis {
font-family: 'MS PGothic', ' Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif;
background: #fbfbfb;
color: #111;
font-size: 12pt;
line-height: 1;
margin: 0;
padding: 4px;
overflow: auto;
white-space: pre;
word-wrap: normal;
}

View File

@ -1,114 +1,124 @@
body {
margin: 0;
padding: 0;
text-align: center;
background: #fff;
color: #555;
font-family: 'Droid Sans', sans-serif;
font-size: 17px;
overflow-y: scroll;
margin: 0;
padding: 0;
text-align: center;
background: #fff;
color: #555;
font-family: 'Droid Sans', sans-serif;
font-size: 15px;
overflow-y: scroll;
}
@media all and (max-width: 40em) {
body {
font-size: 13px;
}
}
h1 {
font-weight: normal;
font-size: 30px;
font-weight: normal;
font-size: 160%;
}
h2 {
font-variant: small-caps;
font-weight: normal;
font-variant: small-caps;
font-weight: normal;
}
h3 {
font-weight: normal;
font-size: 20px;
font-weight: normal;
font-size: 120%;
}
small {
font-size: 87%;
}
#middle {
padding: 0 2em;
position: relative;
padding: 0 2em;
position: relative;
}
#content {
margin: 1.5em 0;
padding: 0;
display: inline-block;
text-align: left;
width: 100%;
margin: 1.5em 0;
padding: 0;
display: inline-block;
text-align: left;
width: 100%;
}
a {
color: #24A2DD;
text-decoration: none;
cursor: pointer; /* for links without [href] */
color: #24A2DD;
text-decoration: none;
cursor: pointer; /* for links without [href] */
}
a:focus {
outline: 2px solid #64C2ED;
outline: 2px solid #64C2ED;
}
a:hover {
color: #34B2ED;
color: #34B2ED;
}
hr {
margin: 1.5em auto;
height: 1px;
border: 0;
border-bottom: 1px solid #eee;
border-top: 1px solid #eee;
box-sizing: content-box;
margin: 1.5em auto;
height: 1px;
border: 0;
border-bottom: 1px solid #eee;
border-top: 1px solid #eee;
box-sizing: content-box;
}
.big-button {
color: #aaa;
color: #aaa;
}
.big-button.active {
background: #f7fbfc;
color: #24A2DD !important;
background: #f7fbfc;
color: #24A2DD !important;
}
.big-button:focus,
.big-button:hover {
background: #f7fbfc;
color: #34B2ED !important;
outline: 0;
background: #f7fbfc;
color: #34B2ED !important;
outline: 0;
}
span.spoiler:before {
content: '[';
color: #000;
content: '[';
color: #000;
}
span.spoiler:after {
content: ']';
color: #000;
content: ']';
color: #000;
}
span.spoiler {
background: #eee;
color: #eee;
background: #eee;
color: #eee;
}
span.spoiler:hover {
color: dimgray;
color: dimgray;
}
blockquote {
border-left: 3px solid #eee;
margin-left: 0;
padding: 0.3em 0.3em 0.3em 0.7em;
background: #fafafa;
color: #444;
border-left: 3px solid #eee;
margin-left: 0;
padding: 0.3em 0.3em 0.3em 0.7em;
background: #fafafa;
color: #444;
}
blockquote :last-child {
margin-bottom: 0;
margin-bottom: 0;
}
.draggable {
cursor: move;
cursor: move;
}
.resizer {
position: absolute;
cursor: nwse-resize;
border: 0.25em solid rgba(0, 0, 0, 0.3);
border-top: 0.25em solid transparent;
border-left: 0.25em solid transparent;
right: 0;
bottom: 0;
position: absolute;
cursor: nwse-resize;
border: 0.25em solid rgba(0, 0, 0, 0.3);
border-top: 0.25em solid transparent;
border-left: 0.25em solid transparent;
right: 0;
bottom: 0;
}

View File

@ -1,261 +1,260 @@
.form-wrapper {
display: table;
margin: 0 auto;
text-align: left !important;
width: 30em;
display: table;
margin: 0 auto;
text-align: left !important;
width: 30em;
}
.form-row {
display: table-row;
display: table-row;
}
.form-label {
width: 1%;
white-space: pre;
width: 1%;
white-space: pre;
}
.form-label,
.form-input {
display: table-cell;
line-height: 37px;
vertical-align: text-bottom;
display: table-cell;
line-height: 37px;
vertical-align: text-bottom;
}
.form-input {
overflow: hidden;
text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
}
.form-row label {
padding-right: 1em;
text-align: right;
padding-right: 1em;
text-align: right;
}
.tag-input,
textarea,
input[type=text],
input[type=password] {
padding: 3px 6px 4px 6px;
border: 1px solid #eee;
box-shadow: 0 1px 2px -1px #e0e0e0 inset;
background: #fafafa;
font-family: 'Inconsolata', monospace;
font-size: 17px;
text-overflow: ellipsis;
width: 100%;
box-sizing: border-box;
padding: 3px 6px 4px 6px;
border: 1px solid #eee;
box-shadow: 0 1px 2px -1px #e0e0e0 inset;
background: #fafafa;
font-family: 'Inconsolata', monospace;
font-size: 100%;
text-overflow: ellipsis;
width: 100%;
box-sizing: border-box;
}
/* remove chrome yellow background for inputs with autocompletion */
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px #fafafa inset;
-webkit-box-shadow: 0 0 0px 1000px #fafafa inset;
}
button::before,
input[type=button]::before {
z-index: -1;
background: linear-gradient(#f5f5f5, #e5e5e5);
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
content: '\a0';
z-index: -1;
background: linear-gradient(#f5f5f5, #e5e5e5);
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
content: '\a0';
}
button,
input[type=button] {
vertical-align: middle;
line-height: normal;
background: transparent;
position: relative;
padding: 2px 15px 3px 15px;
border: 0;
box-shadow: 0 1px 1px 0 #e5e5e5,
0 0 0 1px rgba(0, 0, 0, 0.15) inset,
0 5px 1px -4px white inset;
color: #444;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5), 0 1px 1px rgba(255, 255, 255, 0.5);
font-family: 'Droid Sans', sans-serif;
font-size: 17px;
vertical-align: middle;
line-height: normal;
background: transparent;
position: relative;
padding: 2px 15px 3px 15px;
border: 0;
box-shadow: 0 1px 1px 0 #e5e5e5,
0 0 0 1px rgba(0, 0, 0, 0.15) inset,
0 5px 1px -4px white inset;
color: #444;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5), 0 1px 1px rgba(255, 255, 255, 0.5);
font-family: 'Droid Sans', sans-serif;
font-size: 17px;
}
button:not(:disabled),
input[type=button]:not(:disabled) {
cursor: pointer;
cursor: pointer;
}
button:not(:disabled):hover::before,
input[type=button]:not(:disabled):hover::before {
opacity: .65;
opacity: .65;
}
button:not(:disabled):active,
input[type=button]:not(:disabled):active {
padding: 3px 15px 2px 15px;
padding: 3px 15px 2px 15px;
}
button:not(:disabled):active::before,
input[type=button]:not(:disabled):active::before {
transform: rotate(180deg);
transform: rotate(180deg);
}
button:not(:disabled):focus,
input[type=button]:not(:disabled):focus {
box-shadow: 0 1px 1px 0 #e5e5e5, 0 0 0 2px #64C2ED inset;
box-shadow: 0 1px 1px 0 #e5e5e5, 0 0 0 2px #64C2ED inset;
}
button:disabled {
color: gray;
color: gray;
}
button.highlight::before,
input[type=button].highlight::before {
background: linear-gradient(rgb(160, 221, 251), rgb(101, 188, 239));
background: linear-gradient(rgb(160, 221, 251), rgb(101, 188, 239));
}
button.highlight-red::before,
input[type=button].highlight-red::before {
background: linear-gradient(rgba(255, 181, 143, 1), rgba(255, 148, 122, 1));
background: linear-gradient(rgba(255, 181, 143, 1), rgba(255, 148, 122, 1));
}
button:focus,
textarea:focus,
input:focus {
outline: none;
outline: none;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
border: 0;
}
.tag-input.focused,
textarea:focus,
input:not([type=button]):not(.tag-real-input):focus {
box-shadow: 0 0 0 1px #64C2ED inset;
border-color: #64C2ED;
box-shadow: 0 0 0 1px #64C2ED inset;
border-color: #64C2ED;
}
input[type=radio],
input[type=checkbox] {
opacity: 0;
position: absolute;
opacity: 0;
position: absolute;
}
input[type=radio] + label,
input[type=checkbox] + label {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
input[type=radio] + label::before,
input[type=checkbox] + label::before {
font-family: 'FontAwesome';
display: inline-block;
width: 1em;
text-align: left;
font-family: 'FontAwesome';
display: inline-block;
width: 1em;
text-align: left;
}
input[type=radio] + label::before {
content: "\f1db";
content: "\f1db";
}
input[type=radio]:checked + label::before {
content: "\f192";
content: "\f192";
}
input[type=checkbox] + label::before {
content: "\f096";
content: "\f096";
}
input[type=checkbox]:checked + label::before {
content: "\f046";
content: "\f046";
}
input[type=radio]:focus + label,
input[type=checkbox]:focus + label {
color: #64C2ED;
color: #64C2ED;
}
.file-handler::before {
background: none;
background: none;
}
.file-handler {
box-shadow: none !important;
outline: none !important;
border: 3px dashed #eee !important;
padding: 0.3em 0.5em !Important;
line-height: 140% !important;
text-align: center;
cursor: pointer;
box-shadow: none !important;
outline: none !important;
border: 3px dashed #eee !important;
padding: 0.3em 0.5em !Important;
line-height: 140% !important;
text-align: center;
cursor: pointer;
}
.file-handler.active {
border-color: #64C2ED !important;
background-color: #eeffcc;
border-color: #64C2ED !important;
background-color: #eeffcc;
}
.tag-input {
padding: 1px;
line-height: normal !important;
cursor: text;
padding: 1px;
line-height: normal !important;
cursor: text;
}
.tag-input ul {
list-style-type: none;
display: inline;
margin: 0;
padding: 0;
list-style-type: none;
display: inline;
margin: 0;
padding: 0;
}
.tag-input li {
background: #ddd;
display: inline-block;
font-family: 'Droid Sans', sans-serif;
margin: 1px;
padding: 2px 4px;
font-size: 15px;
background: #ddd;
display: inline-block;
font-family: 'Droid Sans', sans-serif;
margin: 1px;
padding: 2px 4px;
}
.tag-input input {
border: none;
width: auto;
border: none;
width: auto;
}
.tag-input li a {
color: black;
color: black;
}
.tag-input li a.close {
font-size: 14px;
margin-left: 0.75em;
font-size: 85%;
margin-left: 0.5em;
}
.related-tags {
line-height: 200%;
font-size: 15px;
display: none;
margin: 0.5em 0.5em 1em 0.5em;
line-height: 200%;
font-size: 95%;
display: none;
margin: 0.5em 0.5em 1em 0.5em;
}
.related-tags span {
float: left;
float: left;
}
.related-tags ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
.related-tags li {
display: inline-block;
margin: 0 0.5em;
padding: 0;
display: inline-block;
margin: 0 0.5em;
padding: 0;
}
.autocomplete {
position: absolute;
display: none;
z-index: 10;
position: absolute;
display: none;
z-index: 10;
}
.autocomplete ul {
list-style-type: none;
padding: 0 0 !important;
margin: 0 !important;
border: 2px solid #64C2ED;
background: white;
display: block !important;
text-align: left;
list-style-type: none;
padding: 0 0 !important;
margin: 0 !important;
border: 2px solid #64C2ED;
background: white;
display: block !important;
text-align: left;
}
.autocomplete li {
margin: 0;
padding: 0.1em 0.5em !important;
cursor: pointer;
margin: 0;
padding: 0.1em 0.5em !important;
cursor: pointer;
}
.autocomplete li.active {
background: #64C2ED;
background: #64C2ED;
}

View File

@ -1,32 +1,32 @@
#help-view {
max-width: 40em;
margin: 0 auto;
max-width: 40em;
margin: 0 auto;
}
#help-view div[data-tab] {
display: none;
display: none;
}
#help-view ul.tabs {
list-style-type: none;
padding: 0;
margin: 0;
list-style-type: none;
padding: 0;
margin: 0;
}
#help-view ul.tabs {
margin: -0.5em -0.5em 0.5em -0.5em;
margin: -0.5em -0.5em 0.5em -0.5em;
}
#help-view ul.tabs li {
display: inline-block;
margin: 0 0.5em;
display: inline-block;
margin: 0 0.5em;
}
#help-view ul.tabs a {
display: inline-block;
padding: 0.2em 0.5em;
display: inline-block;
padding: 0.2em 0.5em;
}
#help-view table td,
#help-view table th {
padding-right: 1.5em;
padding-right: 1.5em;
}

View File

@ -1,78 +1,78 @@
table.history {
font-size: 80%;
border-spacing: 0;
font-size: 80%;
border-spacing: 0;
}
table.history .addition {
color: #228022;
color: #228022;
}
table.history .addition:before {
content: '+';
content: '+';
}
table.history .removal {
color: #e02222;
color: #e02222;
}
table.history .removal:before {
content: '-';
content: '-';
}
table.history .user img {
vertical-align: middle;
vertical-align: middle;
}
table.history .time,
table.history .user,
table.history .subject {
white-space: nowrap;
white-space: nowrap;
}
table.history .difference {
word-break: break-all;
word-break: break-all;
}
table.history td {
padding: 0.3em 0.5em 0.3em 0.25em;
word-break: break-word;
word-wrap: break-word;
vertical-align: top;
padding: 0.3em 0.5em 0.3em 0.25em;
word-break: break-word;
word-wrap: break-word;
vertical-align: top;
}
table.history tr:nth-child(2n+1) td {
background: #fafafa;
background: #fafafa;
}
table.history ul {
margin: 0;
padding: 0;
list-style-type: none;
display: inline;
margin: 0;
padding: 0;
list-style-type: none;
display: inline;
}
table.history ul:before {
content: ' (';
content: ' (';
}
table.history ul:after {
content: ')';
content: ')';
}
table.history li {
display: inline;
display: inline;
}
table.history li:not(:last-of-type):after {
content: ', ';
content: ', ';
}
#history-wrapper {
text-align: center;
text-align: center;
}
#history-wrapper table {
margin: 0 auto;
text-align: left;
margin: 0 auto;
text-align: left;
}
@media all and (min-width: 77em) {
#history-wrapper table {
width: 70em;
}
#history-wrapper table {
width: 70em;
}
}
@media all and (max-width: 77em) {
#history-wrapper table {
width: 100%;
}
#history-wrapper table {
width: 100%;
}
}

View File

@ -1,39 +1,44 @@
#home {
text-align: center;
text-align: center;
}
#home h1 {
margin-top: 0;
margin-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
#home h1+p {
margin-top: 0;
margin-top: 0;
}
#home .post {
text-align: left;
margin: 0 auto;
display: inline-block;
max-width: 60%;
text-align: center;
margin: 0 auto;
display: inline-block;
max-width: 60%;
min-width: 40em;
}
#home .post .left {
display: inline-block;
float: left;
display: inline-block;
float: left;
margin-right: 0.5em;
}
#home .post .right {
display: inline-block;
float: right;
display: inline-block;
float: right;
margin-left: 0.5em;
}
#home .post-footer,
#home .post-footer img {
vertical-align: middle;
vertical-align: middle;
}
#home .post-footer img {
margin-left: 0.5em;
margin-left: 0.5em;
}
#home .version {
opacity: .4;
font-size: 12px;
opacity: .4;
}
#home .subheader, #home .post-footer {
font-size: 85%;
}

View File

@ -1,6 +1,6 @@
.http-error {
text-align: center;
text-align: center;
}
.http-error img {
margin: 0 auto;
margin: 0 auto;
}

View File

@ -1,22 +1,22 @@
#login-form p {
text-align: center;
text-align: center;
}
#login-form form {
width: 22.5em;
width: 22.5em;
}
#login-form .help {
text-align: center;
text-align: center;
}
#login-form .help ul {
margin: 0;
padding: 0;
display: inline-block;
text-align: left;
list-style-position: inside;
white-space: nowrap;
margin: 0;
padding: 0;
display: inline-block;
text-align: left;
list-style-position: inside;
white-space: nowrap;
}
#login-form .messages {
margin: 0 auto;
margin: 0 auto;
}

View File

@ -1,16 +1,16 @@
.message {
margin: 0 auto 0.2em auto;
padding: 0.4em 0.5em;
text-align: center;
max-width: 40em;
margin: 1em auto;
padding: 0.4em 0.5em;
text-align: center;
max-width: 40em;
}
.message.error {
background: #fdd;
box-shadow: 0 0 0 1px #fcc inset;
background: #fdd;
box-shadow: 0 0 0 1px #fcc inset;
}
.message.info {
background: #def;
box-shadow: 0 0 0 1px #cdf inset;
background: #def;
box-shadow: 0 0 0 1px #cdf inset;
}

View File

@ -1,16 +1,16 @@
.page-list {
text-align: center;
list-style-type: none;
padding: 0;
margin: 1em auto 0 auto;
display: inline-block;
text-align: center;
list-style-type: none;
padding: 0;
margin: 1em auto 0 auto;
display: inline-block;
}
.page-list li {
display: inline-block;
display: inline-block;
}
.page-list li a {
display: inline-block;
padding: 0.4em 1.2em;
display: inline-block;
padding: 0.4em 1.2em;
}

View File

@ -1,48 +1,48 @@
.post-list {
text-align: center;
text-align: center;
}
.post-list .search {
margin: 0 auto 1em auto;
text-align: left;
margin: 0 auto 1em auto;
text-align: left;
}
@media all and (min-width: 57em) {
.post-list .search {
min-width: 50em;
}
.post-list .search {
min-width: 50em;
}
}
@media all and (max-width: 57em) {
.post-list .search {
min-width: 100%;
}
.post-list .search {
min-width: 100%;
}
}
.post-list .search:after {
display: block;
content: '';
clear: both;
display: block;
content: '';
clear: both;
}
.post-list .search input {
max-width: 20em;
max-width: 20em;
}
.post-list .search button {
margin-left: 0.25em;
margin-left: 0.25em;
}
.post-list .search .mass-tag-wrapper {
float: right;
float: right;
}
.post-list .search .mass-tag-wrapper p {
display: inline;
margin-right: 1em;
display: inline;
margin-right: 1em;
}
.post-list ul.safety {
display: inline-block;
list-style-type: none;
padding: 0;
margin: 0;
display: inline-block;
list-style-type: none;
padding: 0;
margin: 0;
}
.post-list ul.safety li {
display: inline-block;
display: inline-block;
}
.post-list ul.safety .safety-safe:before { background: linear-gradient(rgb(224, 248, 218), rgb(213, 233, 208)); }
.post-list ul.safety .safety-sketchy:before { background: linear-gradient(rgb(252, 252, 230), rgb(245, 236, 194)); }
@ -52,154 +52,164 @@
.post-list ul.safety .safety-unsafe.disabled:before { background: linear-gradient(#DDB7B7, #C9A195); }
.post-list ul.posts {
display: flex;
justify-content: center;
align-content: center;
flex-wrap: wrap;
list-style-type: none;
padding: 0;
margin: 0;
display: -webkit-flex;
-webkit-justify-content: center;
-webkit-align-content: center;
-webkit-flex-wrap: wrap;
display: flex;
justify-content: center;
align-content: center;
flex-wrap: wrap;
list-style-type: none;
padding: 0;
margin: 0;
}
.post-list ul.posts .post-small {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
.post-small {
position: relative;
position: relative;
}
.post-small a {
display: inline-block;
margin: 0.2em;
border: 1px solid #999;
z-index: 1;
position: relative;
.post-small .link {
display: block;
margin: 0.3em;
border: 1px solid #999;
z-index: 1;
position: relative;
}
.post-small img {
display: block;
border: 0;
background: white;
display: block;
border: 0;
background: white;
}
.post-small a:focus,
.post-small a:hover {
background: #64C2ED;
border-color: #64C2ED;
box-shadow: 0 0 0 2px #64C2ED;
outline: 0;
.post-small .link:focus,
.post-small .link:hover {
background: #64C2ED;
border-color: #64C2ED;
box-shadow: 0 0 0 2px #64C2ED;
outline: 0;
}
.post-small a:focus img:not(.loading),
.post-small a:hover img:not(.loading) {
opacity: .8 !important;
.post-small .link:focus img:not(.loading),
.post-small .link:hover img:not(.loading) {
opacity: .8 !important;
}
.post-small a .info {
display: none;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #64C2ED;
color: black;
.post-small .link .info {
display: none;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #64C2ED;
color: black;
}
.post-small a .info ul {
list-style-type: none;
padding: 0;
margin: 0;
.post-small .link .info ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.post-small a .info li {
display: inline-block;
margin: 0.1em 0.5em;
padding: 0;
.post-small .link .info li {
display: inline-block;
margin: 0.1em 0.5em;
padding: 0;
}
.post-small a:focus .info,
.post-small a:hover .info {
display: block;
.post-small .link:focus .info,
.post-small .link:hover .info {
display: block;
}
.post-small:not(.post-type-image) a::before {
display: block;
content: '';
z-index: 2;
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border-top: 50px solid red;
border-left: 50px solid transparent;
.post-small:not(.post-type-image) .link::before {
display: block;
content: '';
z-index: 2;
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border-top: 50px solid red;
border-left: 50px solid transparent;
}
.post-small:not(.post-type-image) a::after {
display: block;
content: '...';
z-index: 3;
position: absolute;
top: -35px; /* 50 * sqrt(2) / 2 */
right: -35px;
width: 71px; /* 50 * sqrt(2) */
height: 71px;
line-height: 122px; /* 71 * 2 - 11 (font-size) - padding */
transform: rotate(45deg);
text-align: center;
color: white;
font-size: 15px;
.post-small:not(.post-type-image) .link::after {
pointer-events: none;
display: block;
content: '...';
z-index: 3;
position: absolute;
top: -35px; /* 50 * sqrt(2) / 2 */
right: -35px;
width: 71px; /* 50 * sqrt(2) */
height: 71px;
line-height: 122px; /* 71 * 2 - 11 (font-size) - padding */
transform: rotate(45deg);
text-align: center;
color: white;
font-size: 15px;
}
.post-small.post-type-youtube a::after {
font-size: 13px;
content: 'youtube';
.post-small.post-type-youtube .link::after {
font-size: 13px;
content: 'youtube';
}
.post-small.post-type-video a::after {
content: 'video';
.post-small.post-type-video .link::after {
content: 'video';
}
.post-small.post-type-flash a::after {
content: 'flash';
.post-small.post-type-flash .link::after {
content: 'flash';
}
.post-small.post-type-animation .link::after {
content: 'anim';
}
.post-small .action {
display: none;
position: absolute;
z-index: 3;
left: 0;
right: 0;
top: 50%;
bottom: 0;
pointer-events: none;
display: none;
position: absolute;
z-index: 3;
left: 0;
right: 0;
top: 50%;
bottom: 0;
pointer-events: none;
}
.post-small .action button {
padding: 0.5em 1em;
height: 1em;
line-height: 1em;
display: block;
margin: -1em auto 0 auto;
box-sizing: content-box;
opacity: .7;
box-shadow: none;
pointer-events: auto;
padding: 0.5em 1em;
height: 1em;
line-height: 1em;
display: block;
margin: -1em auto 0 auto;
box-sizing: content-box;
opacity: .7;
box-shadow: none;
pointer-events: auto;
}
.post-small .action button:focus {
box-shadow: 0 0 0 2px #64C2ED inset !important;
box-shadow: 0 0 0 2px #64C2ED inset !important;
}
.tagged .action button,
.untagged .action button {
border: 1px solid black;
font-weight: bold;
text-shadow: none;
border: 1px solid black;
font-weight: bold;
text-shadow: none;
}
.untagged .action button::before {
background: red;
background: red;
}
.untagged .action button {
color: white;
color: white;
}
.tagged .action button::before {
background: lime;
background: lime;
}
.tagged .action button {
color: black;
color: black;
}

View File

@ -1,175 +1,146 @@
#post-upload-step1 {
display: table;
width: 30em;
margin: 0 auto;
display: table;
width: 30em;
margin: 0 auto;
}
#post-upload-step1 .file-handler {
padding: 3.5em !important;
width: 100%;
padding: 3.5em !important;
width: 100%;
}
#post-upload-step1 .url-handler {
margin-top: 0.5em;
position: relative;
margin-top: 0.5em;
position: relative;
}
#post-upload-step1 .url-handler .input-wrapper {
margin-right: 8.5em;
margin-right: 9.5em;
}
#post-upload-step1 .url-handler button {
position: absolute;
top: 0;
right: 0;
width: 8em;
position: absolute;
top: 0;
right: 0;
width: 8em;
}
#post-upload-step2 .hybrid-view {
text-align: center;
text-align: center;
}
@media all and (min-width: 62.5em) {
#post-upload-step2 .hybrid-window:first-child {
width: 40%;
margin-right: 1em;
float: left;
}
#post-upload-step2 .hybrid-window:last-child {
display: inline-block;
width: 57.5%;
}
#post-upload-step2 .hybrid-window:first-child {
width: 40%;
margin-right: 1em;
float: left;
}
#post-upload-step2 .hybrid-window:last-child {
display: inline-block;
width: 57.5%;
}
}
#post-upload-step2 .thumbnail img {
border: 1px solid black;
vertical-align: middle;
display: block;
border: 1px solid black;
vertical-align: middle;
display: block;
}
#post-upload-step2 table {
border-spacing: 0;
table-layout: fixed;
width: 100%;
border-spacing: 0;
table-layout: fixed;
width: 100%;
}
#post-upload-step2 table td,
#post-upload-step2 table th {
padding: 0.2em 0.5em;
text-align: center;
padding: 0.2em 0.5em;
text-align: center;
}
#post-upload-step2 table th {
font-weight: normal;
font-weight: normal;
}
#post-upload-step2 table tr.selected {
background: #f7fbfc;
background: #f7fbfc;
}
#post-upload-step2 table .checkbox {
width: 30px;
padding: 0.2em 0;
width: 30px;
padding: 0.2em 0;
}
#post-upload-step2 table .checkbox input {
margin: 0 auto;
margin: 0 auto;
}
#post-upload-step2 table .thumbnail {
width: 40px;
padding: 0.2em 0;
width: 40px;
padding: 0.2em 0;
}
#post-upload-step2 table .safety {
width: 60px;
padding: 0.2em 0;
width: 60px;
padding: 0.2em 0;
}
#post-upload-step2 table .tags {
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
#post-upload-step2 table .safety {
text-align: center;
text-align: center;
}
#post-upload-step2 table .safety [class^=safety-] {
box-shadow: inset 0 0 0 3px rgba(0,0,0,0.1);
width: 25px;
height: 25px;
margin: 0 auto;
box-shadow: inset 0 0 0 3px rgba(0,0,0,0.1);
width: 25px;
height: 25px;
margin: 0 auto;
}
#post-upload-step2 table .safety-safe { background: #b2efa2; }
#post-upload-step2 table .safety-sketchy { background: #f0e4a8; }
#post-upload-step2 table .safety-unsafe { background: #fbc6b6; }
#post-upload-step2 table .thumbnail img {
width: 30px;
height: 30px;
display: inline-block;
background-size: 30px 30px;
width: 30px;
height: 30px;
display: inline-block;
background-size: 30px 30px;
}
#post-upload-step2 .operations {
list-style-type: none;
margin: 0;
padding: 0;
list-style-type: none;
margin: 0;
padding: 0;
}
#post-upload-step2 .operations li {
display: inline-block;
margin: 0.3em 0.3em 0 0;
display: inline-block;
margin: 0.3em 0.3em 0 0;
}
#post-upload-step2 .operations .stop {
display: none;
display: none;
}
#post-upload-step2 form {
width: 100%;
margin: 0 auto;
overflow: hidden;
text-align: left;
width: 100%;
margin: 0 auto;
overflow: hidden;
text-align: left;
}
#post-upload-step2 .messages {
margin-bottom: 1em;
margin: 1em 0;
}
#post-upload-step2 .form-slider {
text-align: center;
text-align: center;
}
#post-upload-step2 .form-slider .thumbnail img {
max-width: 100%;
max-height: 300px;
margin: 0 auto 1em auto;
max-width: 100%;
max-height: 450px;
margin: 0 auto 1em auto;
}
#post-upload-step2 .file-name .form-input {
word-break: break-all;
word-break: break-all;
}
#post-upload-step2,
.template {
display: none;
}
#lightbox {
display: none;
position: absolute;
pointer-events: none;
position: absolute;
margin-left: 10px;
}
#lightbox img {
max-width: 400px;
max-height: 400px;
background: white;
border: 0.5em solid white;
box-shadow: 0 0 0 1px #eee;
position: relative;
}
#lightbox:after {
content: '';
position: absolute;
left: -8px;
top: 50%;
margin-top: -8px;
width: 12px;
height: 12px;
background: white;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
transform: rotate(45deg);
display: none;
}
#uploading-alert {
display: none;
text-align: left;
display: none;
text-align: left;
}

View File

@ -1,254 +1,279 @@
.post-type-video video {
max-width: 100%;
}
.post-type-image .image-wrapper {
max-width: 100%;
position: relative;
}
.post-type-image .image-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.post-type-youtube iframe {
width: 800px;
height: 600px;
border: 0;
}
#post-current-search-wrapper {
text-align: center;
text-align: center;
}
#post-current-search {
margin: 0 auto 1em auto;
display: inline-block;
margin: 0 auto 1em auto;
display: inline-block;
}
#post-current-search a {
margin: 0 2em;
margin: 0 2em;
}
#post-current-search a,
#post-current-search div {
display: inline-block;
display: inline-block;
}
#post-current-search a:not(.enabled) {
color: silver;
cursor: text;
color: silver;
cursor: text;
}
#post-view-wrapper #sidebar {
line-height: 1.33em;
font-size: 90%;
line-height: 1.33em;
}
#post-view-wrapper #sidebar h1 {
margin-top: 1.5em;
}
#post-view-wrapper #sidebar h1:first-of-type {
margin-top: 0;
#post-view-wrapper #sidebar .box {
margin-bottom: 1.5em;
text-align: left;
}
@media all and (min-width: 62.5em) {
#post-view-wrapper {
display: flex;
}
#post-view-wrapper {
display: -webkit-flex;
display: flex;
}
#post-view-wrapper #sidebar {
min-width: 15em;
margin-right: 1em;
flex: 1;
}
#post-view-wrapper #sidebar {
min-width: 15em;
margin-right: 1em;
-webkit-flex: 1;
flex: 1;
}
#post-view-wrapper #post-view {
flex: 5;
}
#post-view-wrapper #post-view {
-webkit-flex: 5;
flex: 5;
}
}
@media all and (max-width: 62.5em) {
#post-view-wrapper {
display: flex;
flex-direction: column;
}
#post-view-wrapper #sidebar {
order: 2;
margin-bottom: 1em;
}
#post-view-wrapper #post-view {
margin: 0 auto;
max-width: 100%;
order: 1;
}
#post-view-wrapper {
display: -webkit-flex;
-webkit-flex-direction: column;
display: flex;
flex-direction: column;
}
#post-view-wrapper #sidebar {
order: 2;
margin-bottom: 1em;
text-align: center;
}
#post-view-wrapper #sidebar .box {
display: inline-block;
width: 15em;
vertical-align: top;
}
#post-view-wrapper #post-view {
margin: 0 auto;
width: 100%;
order: 1;
}
}
#post-view-wrapper .favorites li {
display: inline-block;
margin: 0 0.25em 0.25em 0;
display: inline-block;
margin: 0 0.25em 0.25em 0;
}
#sidebar ul {
list-style-type: none;
margin: 0;
padding: 0;
list-style-type: none;
margin: 0;
padding: 0;
}
#sidebar .other-info li {
display: block;
word-break: break-all;
padding-left: 1em;
text-indent: -1em;
display: block;
word-break: break-all;
padding-left: 1em;
text-indent: -1em;
}
#sidebar .tags li {
display: block;
word-break: break-all;
padding-left: 20px;
text-indent: -20px;
display: block;
word-break: break-all;
padding-left: 20px;
text-indent: -20px;
}
#sidebar .tags .tag-wrapper {
max-width: 100%;
position: relative;
display: inline-block;
max-width: 100%;
position: relative;
display: inline-block;
}
#sidebar .tags li a.tag-edit {
margin-left: 20px;
margin-left: 20px;
}
#sidebar .tags li .tag-name {
margin-right: 0.8em;
margin-right: 0.8em;
}
#sidebar .tags li .usages {
color: silver;
color: silver;
}
#sidebar .author-box img {
float: left;
margin-right: 0.5em;
float: left;
margin-right: 0.5em;
}
#sidebar .author-box .author-name {
font-weight: bold;
font-weight: bold;
}
#sidebar .other-info {
margin-top: 1em;
line-height: 150%;
margin-top: 1em;
line-height: 150%;
}
#sidebar .fit-mode a {
opacity: .25;
}
#sidebar .fit-mode a.active {
opacity: 1;
}
#sidebar .essential {
display: flex;
justify-content: space-around;
margin-bottom: 2em;
max-width: 30em;
display: -webkit-flex;
-webkit-justify-content: space-around;
display: flex;
justify-content: space-around;
margin-bottom: 2em;
}
#sidebar .essential li {
display: block;
margin: 0 0.25em;
vertical-align: top;
display: block;
margin: 0 0.25em;
vertical-align: top;
}
#sidebar .essential li i.fa {
font-size: 30px;
font-size: 200%;
}
#sidebar .essential li a {
display: block;
text-align: center;
font-size: 12px;
display: block;
text-align: center;
font-size: 87%;
}
#post-view #post-edit-target {
padding: 1em;
position: absolute;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 1em 0.5em rgba(255, 255, 255, 0.8);
z-index: 2;
display: none;
padding: 1em;
width: 50%;
min-width: 30em;
position: absolute;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 1em 0.5em rgba(255, 255, 255, 0.8);
z-index: 2;
display: none;
}
#post-edit-target .form-wrapper {
min-width: 100%;
}
#post-view>* {
z-index: -1;
z-index: -1;
}
#post-edit-target .advanced-trigger .form-input {
overflow: auto; /* fix browser's outline around the link being cut due to overflow: hidden; */
}
#post-edit-target .file-handler {
margin: 0.5em 0;
margin: 0.5em 0;
}
#post-view-wrapper .post-history-wrapper {
padding: 1em 0;
display: none;
word-break: break-all;
padding: 1em 0;
display: none;
word-break: break-all;
}
.post-content {
position: relative;
margin-bottom: 0.5em;
position: relative;
margin-bottom: 0.5em;
}
.post-content .object-wrapper {
max-width: 100%;
position: relative;
}
.post-content .object-wrapper img,
.post-content .object-wrapper object,
.post-content .object-wrapper iframe,
.post-content .object-wrapper video {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
border: 0;
}
.post-content .object-wrapper video {
background: black;
}
.post-notes-target {
position: absolute;
pointer-events: none;
z-index: 1;
position: absolute;
pointer-events: none;
z-index: 1;
}
.post-notes {
position: relative;
width: 100%;
height: 100%;
position: relative;
width: 100%;
height: 100%;
}
.post-note-edit {
pointer-events: auto;
display: none;
position: fixed;
z-index: 3;
background: white;
border: 1px solid black;
padding: 1em;
left: 30%;
top: 20%;
pointer-events: auto;
display: none;
position: fixed;
z-index: 3;
background: white;
border: 1px solid black;
padding: 1em;
left: 30%;
top: 20%;
}
.post-note-edit textarea {
width: 25em;
height: 5em;
display: block;
width: 25em;
height: 5em;
display: block;
}
.post-note-edit .actions {
pointer-events: none;
pointer-events: none;
}
.post-note-edit button {
pointer-events: auto;
margin-top: 0.5em;
pointer-events: auto;
margin-top: 0.5em;
}
.post-note-edit .actions button:not(:last-child) {
margin-right: 0.5em;
margin-right: 0.5em;
}
.post-note {
pointer-events: auto;
position: absolute;
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(0, 0, 0, 0.3);
outline: 0;
pointer-events: auto;
position: absolute;
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(0, 0, 0, 0.3);
font-size: 12pt;
}
.post-note:focus {
border-color: rgba(255, 0, 0, 0.3);
background-color: rgba(255, 225, 225, 0.3);
}
.post-note .text-wrapper {
position: absolute;
display: none;
z-index: 1;
top: calc(100%);
left: -1px;
padding-top: 0.5em;
cursor: pointer;
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
position: absolute;
display: none;
z-index: 1;
top: calc(100%);
left: -1px;
padding-top: 0.5em;
cursor: pointer;
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
max-width: 22.5em;
}
.post-note .text {
padding: 0.5em;
background: lemonchiffon;
border: 1px solid black;
}
.post-note:hover .text-wrapper {
display: block;
padding: 0.5em;
background: lemonchiffon;
border: 1px solid black;
}
.post-note .text p:first-of-type {
margin-top: 0;
margin-top: 0;
}
.post-note .text p:last-of-type {
margin-bottom: 0;
margin-bottom: 0;
}

View File

@ -1,10 +1,10 @@
#registration-form p {
text-align: center;
text-align: center;
}
#registration-form form {
width: 25em;
width: 25em;
}
#registration-form .messages {
margin: 0 auto;
margin: 0 auto;
}

View File

@ -1,96 +1,83 @@
#tag-list-wrapper {
text-align: center;
text-align: center;
}
#tag-list {
width: 100%;
display: inline-block;
margin: 0 auto;
width: 100%;
display: inline-block;
margin: 0 auto;
}
@media all and (min-width: 67em) {
#tag-list {
width: 60em;
}
#tag-list {
width: 60em;
}
}
@media all and (max-width: 40em) {
#tag-list .implications,
#tag-list .suggestions {
display: none;
}
#tag-list .implications,
#tag-list .suggestions {
display: none;
}
}
#tag-list form {
float: left;
white-space: nowrap;
float: left;
white-space: nowrap;
}
#tag-list form input {
width: 15em;
width: 15em;
}
#tag-list ul.order {
float: right;
list-style-type: none;
padding: 0;
margin: 0 -0.5em 0 0;
float: right;
list-style-type: none;
padding: 0;
margin: 0 -0.5em 0 0;
}
#tag-list .search:after {
display: block;
content: '';
clear: both;
display: block;
content: '';
clear: both;
}
#ta-list ul.order {
margin: -0.5em -0.5em 0.5em -0.5em;
margin: -0.5em -0.5em 0.5em -0.5em;
}
#tag-list ul.order li {
display: inline-block;
margin: 0 0.5em;
display: inline-block;
margin: 0 0.5em;
}
#tag-list ul.order a {
display: inline-block;
padding: 0.2em 0.5em;
display: inline-block;
padding: 0.2em 0.5em;
}
#tag-list table {
width: 100%;
text-align: left;
margin: 1em auto;
width: 100%;
text-align: left;
margin: 1em auto;
}
#tag-list th:not(:last-child),
#tag-list td:not(:last-child) {
padding-right: 1.5em;
padding-right: 1.5em;
}
#tag-list th {
font-weight: normal;
font-weight: normal;
}
#tag-list .fa-check {
opacity: .2;
opacity: .2;
}
#tag-list .banned,
#tag-list .usages {
text-align: center;
text-align: center;
}
#tag-list .implications,
#tag-list .suggestions {
word-break: break-all;
word-break: break-all;
}
.tag-category-character,
.tag-category-character a {
color: #0a0;
}
.tag-category-copyright,
.tag-category-copyright a {
color: #a0a;
}
.tag-category-artist,
.tag-category-artist a {
color: #a00;
}
.tag-category-meta,
.tag-category-meta a {
color: #aaa;
*[class*='tag-category-']:not(.tag-category-default) a {
color: inherit;
}

View File

@ -1,43 +1,43 @@
#tag-view {
margin: 0 auto;
max-width: 45em;
text-align: center;
margin: 0 auto;
max-width: 45em;
text-align: center;
}
#tav-view .header {
text-align: left;
display: inline-block;
margin: 0 auto;
text-align: left;
display: inline-block;
margin: 0 auto;
}
#tag-view .header h1 {
margin-top: 0;
margin-top: 0;
}
#tag-view h3 {
margin-bottom: 0.5em;
margin-bottom: 0.5em;
}
#tag-view form {
text-align: left;
max-width: 30em;
margin: 0 auto;
text-align: left;
max-width: 30em;
margin: 0 auto;
}
#tag-view p {
margin: 0 0 0.5em 0;
line-height: normal;
margin: 0 0 0.5em 0;
line-height: normal;
}
#tag-view small {
font-size: 12px;
font-size: 0.85em;
}
#tag-view .siblings ul {
list-style-type: none;
margin: 0;
padding: 0;
list-style-type: none;
margin: 0;
padding: 0;
}
#tag-view .siblings ul li {
display: inline-block;
margin: 0em 0.5em;
line-height: normal;
display: inline-block;
margin: 0em 0.5em;
line-height: normal;
}

View File

@ -1,54 +1,54 @@
#top-navigation {
width: 100%;
width: 100%;
}
#top-navigation ul {
list-style-type: none;
padding: 0 2em;
margin: 0;
list-style-type: none;
padding: 0 2em;
margin: 0;
}
#top-navigation li {
display: inline-block;
display: inline-block;
}
#top-navigation li a {
display: inline-block;
text-transform: lowercase;
font-variant: small-caps;
padding: 0.5em 1em;
font-size: 15px;
display: inline-block;
text-transform: lowercase;
font-variant: small-caps;
padding: 0.5em 1em;
font-size: 0.9em;
}
#top-navigation li a:focus,
#top-navigation li a:hover {
outline: 0;
outline: 0;
}
#top-navigation li:first-child a {
margin-left: -1em;
margin-left: -1em;
}
#top-navigation li:last-child a {
margin-right: -1em;
margin-right: -1em;
}
#top-navigation i {
font-size: 40px;
margin: 0 10px 5px;
font-size: 3em;
margin: 0 10px 5px;
}
#top-navigation .accesskey:before {
position: absolute;
display: inline-block;
right: 0;
left: 0;
content: '\a0';
border-bottom: 1px solid;
opacity: .35;
height: 90%;
position: absolute;
display: inline-block;
right: 0;
left: 0;
content: '\a0';
border-bottom: 1px solid;
opacity: .35;
height: 90%;
}
#top-navigation .accesskey {
position: relative;
text-decoration: none;
position: relative;
text-decoration: none;
}

View File

@ -1,58 +1,60 @@
#user-list {
min-width: 20em;
text-align: center;
min-width: 20em;
text-align: center;
}
#user-list ul {
list-style-type: none;
padding: 0;
margin: 0;
list-style-type: none;
padding: 0;
margin: 0;
}
#user-list ul.order {
margin: -0.5em -0.5em 0.5em -0.5em;
margin: -0.5em -0.5em 0.5em -0.5em;
}
#user-list ul.order li {
display: inline-block;
margin: 0 0.5em;
display: inline-block;
margin: 0 0.5em;
}
#user-list ul.order a {
display: inline-block;
padding: 0.2em 0.5em;
display: inline-block;
padding: 0.2em 0.5em;
}
#user-list .users {
display: inline-block;
margin: 0 auto;
display: inline-block;
margin: 0 auto;
}
#user-list .users li {
text-align: left;
margin: 0.5em 0;
text-align: left;
margin: 0.5em 0;
}
#user-list li:after {
clear: left;
content: '';
display: block;
clear: left;
content: '';
display: block;
}
#user-list .user img {
vertical-align: top;
margin-right: 1em;
display: block;
vertical-align: top;
display: block;
}
#user-list .user>a {
display: block;
float: left;
#user-list .user .avatar {
float: left;
margin-right: 1em;
}
#user-list .user .avatar a {
display: block;
}
#user-list .user .details {
float: left;
vertical-align: top;
float: left;
vertical-align: top;
}
#user-list .user h1 {
margin-top: 0;
font-weight: normal;
font-size: 16pt;
margin-top: 0;
font-weight: normal;
font-size: 1.25em;
}

View File

@ -1,57 +1,57 @@
#user-view {
min-width: 30em;
text-align: center;
min-width: 30em;
text-align: center;
}
#user-view .side {
text-align: center;
width: 150px;
float: left;
text-align: center;
width: 150px;
float: left;
}
#user-view .top {
display: inline-block;
margin: 0 auto;
display: inline-block;
margin: 0 auto;
}
#user-view ul {
display: inline-block;
list-style-type: none;
text-align: left;
margin: 0;
padding: 0;
display: inline-block;
list-style-type: none;
text-align: left;
margin: 0;
padding: 0;
}
#user-view ul.links {
list-style-type: disc;
list-style-position: inside;
white-space: nowrap;
list-style-type: disc;
list-style-position: inside;
white-space: nowrap;
}
#user-view ul.links li a {
padding-left: 0;
margin-left: 0;
padding-left: 0;
margin-left: 0;
}
#user-view ul a {
display: inline-block;
padding: 0.2em 0.5em;
display: inline-block;
padding: 0.2em 0.5em;
}
#user-view .tab {
display: none;
margin-top: 1.5em;
clear: both;
display: none;
margin-top: 1.5em;
clear: both;
}
#user-view .tab.active {
display: block;
display: block;
}
#user-view .tab.basic-info table {
margin: 0 auto;
text-align: left;
margin: 0 auto;
text-align: left;
}
#user-view .tab.basic-info td {
padding: 0.2em 0.5em;
vertical-align: top;
padding: 0.2em 0.5em;
vertical-align: top;
}

View File

@ -5,7 +5,7 @@
data-version="dev"
data-build-time=""
data-max-post-size="10485760"
data-tag-categories='["meta","character","artist","copyright"]'>
data-tag-categories='[["meta","meta","#aaa"],["character","character","#0a0"],["artist","artist","#a00"],["copyright","copyright","#a0a"]]'>
<!-- /build -->
<!-- build:template
<head
@ -15,6 +15,7 @@
data-tag-categories='<%= JSON.stringify(tagCategories).replace(/'/g, '&#039;') %>'>
/build -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<!-- build:remove -->
<title>szurubooru</title>
@ -23,11 +24,26 @@
<title><%= serviceName %></title>
/build -->
<!-- build:remove -->
<style type="text/css">
.tag-category-character { color: #0a0; }
.tag-category-copyright { color: #a0a; }
.tag-category-artist { color: #a00; }
.tag-category-meta { color: #aaa; }
</style>
<!-- /build -->
<!-- build:template
<link rel="stylesheet" type="text/css" href="app.min.css?<%= timestamp %>"/>
<style type="text/css">
<% _.each(tagCategories, function(item) {
var type = item[0];
var color = item[2];
%>.tag-category-<%= type %>{color:<%=color%>;}<%
}); %>
</style>
/build -->
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="/lib/font-awesome/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Droid+Sans:400,700"/>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Inconsolata">
@ -138,7 +154,7 @@
<script type="text/javascript" src="/js/Presenters/PostPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/GlobalCommentListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/PostCommentListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/CommentListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/TagListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/TagPresenter.js"></script>

View File

@ -2,124 +2,130 @@ var App = App || {};
App.API = function(_, jQuery, promise, appState) {
var baseUrl = '/api/';
var AJAX_UNSENT = 0;
var AJAX_OPENED = 1;
var AJAX_HEADERS_RECEIVED = 2;
var AJAX_LOADING = 3;
var AJAX_DONE = 4;
var baseUrl = '/api/';
var AJAX_UNSENT = 0;
var AJAX_OPENED = 1;
var AJAX_HEADERS_RECEIVED = 2;
var AJAX_LOADING = 3;
var AJAX_DONE = 4;
var cache = {};
var cache = {};
function get(url, data) {
return request('GET', url, data);
}
function get(url, data) {
return request('GET', url, data);
}
function post(url, data) {
return request('POST', url, data);
}
function post(url, data) {
return request('POST', url, data);
}
function put(url, data) {
return request('PUT', url, data);
}
function put(url, data) {
return request('PUT', url, data);
}
function _delete(url, data) {
return request('DELETE', url, data);
}
function _delete(url, data) {
return request('DELETE', url, data);
}
function getCacheKey(method, url, data) {
return JSON.stringify({method: method, url: url, data: data});
}
function getCacheKey(method, url, data) {
return JSON.stringify({method: method, url: url, data: data});
}
function clearCache() {
cache = {};
}
function clearCache() {
cache = {};
}
function request(method, url, data) {
if (method === 'GET') {
return requestWithCache(method, url, data);
}
clearCache();
return requestWithAjax(method, url, data);
}
function request(method, url, data) {
if (method === 'GET') {
return requestWithCache(method, url, data);
}
clearCache();
return requestWithAjax(method, url, data);
}
function requestWithCache(method, url, data) {
var cacheKey = getCacheKey(method, url, data);
if (_.has(cache, cacheKey)) {
return promise.make(function(resolve, reject) {
resolve(cache[cacheKey]);
});
}
function requestWithCache(method, url, data) {
var cacheKey = getCacheKey(method, url, data);
if (_.has(cache, cacheKey)) {
return promise.make(function(resolve, reject) {
resolve(cache[cacheKey]);
});
}
return promise.make(function(resolve, reject) {
promise.wait(requestWithAjax(method, url, data))
.then(function(response) {
setCache(method, url, data, response);
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
return promise.make(function(resolve, reject) {
promise.wait(requestWithAjax(method, url, data))
.then(function(response) {
setCache(method, url, data, response);
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
function setCache(method, url, data, response) {
var cacheKey = getCacheKey(method, url, data);
cache[cacheKey] = response;
}
function setCache(method, url, data, response) {
var cacheKey = getCacheKey(method, url, data);
cache[cacheKey] = response;
}
function requestWithAjax(method, url, data) {
var fullUrl = baseUrl + '/' + url;
fullUrl = fullUrl.replace(/\/{2,}/, '/');
function requestWithAjax(method, url, data) {
var fullUrl = baseUrl + '/' + url;
fullUrl = fullUrl.replace(/\/{2,}/, '/');
var xhr = null;
var apiPromise = promise.make(function(resolve, reject) {
xhr = jQuery.ajax({
headers: {
'X-Authorization-Token': appState.get('loginToken') || '',
},
success: function(data, textStatus, xhr) {
resolve({
status: xhr.status,
json: stripMeta(data)});
},
error: function(xhr, textStatus, errorThrown) {
reject({
status: xhr.status,
json: xhr.responseJSON ?
stripMeta(xhr.responseJSON) :
{error: errorThrown}});
},
type: method,
url: fullUrl,
data: data,
});
});
apiPromise.xhr = xhr;
return apiPromise;
}
var xhr = null;
var apiPromise = promise.make(function(resolve, reject) {
var options = {
headers: {
'X-Authorization-Token': appState.get('loginToken') || '',
},
success: function(data, textStatus, xhr) {
resolve({
status: xhr.status,
json: stripMeta(data)});
},
error: function(xhr, textStatus, errorThrown) {
reject({
status: xhr.status,
json: xhr.responseJSON ?
stripMeta(xhr.responseJSON) :
{error: errorThrown}});
},
type: method,
url: fullUrl,
data: data,
cache: false,
};
if (data instanceof FormData) {
options.processData = false;
options.contentType = false;
}
xhr = jQuery.ajax(options);
});
apiPromise.xhr = xhr;
return apiPromise;
}
function stripMeta(data) {
var result = {};
_.each(data, function(v, k) {
if (!k.match(/^__/)) {
result[k] = v;
}
});
return result;
}
function stripMeta(data) {
var result = {};
_.each(data, function(v, k) {
if (!k.match(/^__/)) {
result[k] = v;
}
});
return result;
}
return {
get: get,
post: post,
put: put,
delete: _delete,
return {
get: get,
post: post,
put: put,
delete: _delete,
AJAX_UNSENT: AJAX_UNSENT,
AJAX_OPENED: AJAX_OPENED,
AJAX_HEADERS_RECEIVED: AJAX_HEADERS_RECEIVED,
AJAX_LOADING: AJAX_LOADING,
AJAX_DONE: AJAX_DONE,
};
AJAX_UNSENT: AJAX_UNSENT,
AJAX_OPENED: AJAX_OPENED,
AJAX_HEADERS_RECEIVED: AJAX_HEADERS_RECEIVED,
AJAX_LOADING: AJAX_LOADING,
AJAX_DONE: AJAX_DONE,
};
};

View File

@ -2,198 +2,199 @@ var App = App || {};
App.Auth = function(_, jQuery, util, api, appState, promise) {
var privileges = {
register: 'register',
listUsers: 'listUsers',
viewUsers: 'viewUsers',
viewAllAccessRanks: 'viewAllAccessRanks',
viewAllEmailAddresses: 'viewAllEmailAddresses',
changeAccessRank: 'changeAccessRank',
changeOwnAvatarStyle: 'changeOwnAvatarStyle',
changeOwnEmailAddress: 'changeOwnEmailAddress',
changeOwnName: 'changeOwnName',
changeOwnPassword: 'changeOwnPassword',
changeAllAvatarStyles: 'changeAllAvatarStyles',
changeAllEmailAddresses: 'changeAllEmailAddresses',
changeAllNames: 'changeAllNames',
changeAllPasswords: 'changeAllPasswords',
deleteOwnAccount: 'deleteOwnAccount',
deleteAllAccounts: 'deleteAllAccounts',
banUsers: 'banUsers',
var privileges = {
register: 'register',
listUsers: 'listUsers',
viewUsers: 'viewUsers',
viewAllAccessRanks: 'viewAllAccessRanks',
viewAllEmailAddresses: 'viewAllEmailAddresses',
changeAccessRank: 'changeAccessRank',
changeOwnAvatarStyle: 'changeOwnAvatarStyle',
changeOwnEmailAddress: 'changeOwnEmailAddress',
changeOwnName: 'changeOwnName',
changeOwnPassword: 'changeOwnPassword',
changeAllAvatarStyles: 'changeAllAvatarStyles',
changeAllEmailAddresses: 'changeAllEmailAddresses',
changeAllNames: 'changeAllNames',
changeAllPasswords: 'changeAllPasswords',
deleteOwnAccount: 'deleteOwnAccount',
deleteAllAccounts: 'deleteAllAccounts',
banUsers: 'banUsers',
listPosts: 'listPosts',
viewPosts: 'viewPosts',
uploadPosts: 'uploadPosts',
uploadPostsAnonymously: 'uploadPostsAnonymously',
deletePosts: 'deletePosts',
featurePosts: 'featurePosts',
changePostSafety: 'changePostSafety',
changePostSource: 'changePostSource',
changePostTags: 'changePostTags',
changePostContent: 'changePostContent',
changePostThumbnail: 'changePostThumbnail',
changePostRelations: 'changePostRelations',
changePostFlags: 'changePostFlags',
listPosts: 'listPosts',
viewPosts: 'viewPosts',
uploadPosts: 'uploadPosts',
uploadPostsAnonymously: 'uploadPostsAnonymously',
deletePosts: 'deletePosts',
featurePosts: 'featurePosts',
changePostSafety: 'changePostSafety',
changePostSource: 'changePostSource',
changePostTags: 'changePostTags',
changePostContent: 'changePostContent',
changePostThumbnail: 'changePostThumbnail',
changePostRelations: 'changePostRelations',
changePostFlags: 'changePostFlags',
addPostNotes: 'addPostNotes',
editPostNotes: 'editPostNotes',
deletePostNotes: 'deletePostNotes',
addPostNotes: 'addPostNotes',
editPostNotes: 'editPostNotes',
deletePostNotes: 'deletePostNotes',
listComments: 'listComments',
addComments: 'addComments',
editOwnComments: 'editOwnComments',
editAllComments: 'editAllComments',
deleteOwnComments: 'deleteOwnComments',
deleteAllComments: 'deleteAllComments',
deleteTags: 'deleteTags',
mergeTags: 'mergeTags',
listComments: 'listComments',
addComments: 'addComments',
editOwnComments: 'editOwnComments',
editAllComments: 'editAllComments',
deleteOwnComments: 'deleteOwnComments',
deleteAllComments: 'deleteAllComments',
deleteTags: 'deleteTags',
mergeTags: 'mergeTags',
listTags: 'listTags',
massTag: 'massTag',
changeTagName: 'changeTagName',
changeTagCategory: 'changeTagCategory',
changeTagImplications: 'changeTagImplications',
changeTagSuggestions: 'changeTagSuggestions',
banTags: 'banTags',
listTags: 'listTags',
massTag: 'massTag',
changeTagName: 'changeTagName',
changeTagCategory: 'changeTagCategory',
changeTagImplications: 'changeTagImplications',
changeTagSuggestions: 'changeTagSuggestions',
banTags: 'banTags',
viewHistory: 'viewHistory',
};
viewHistory: 'viewHistory',
};
function loginFromCredentials(userNameOrEmail, password, remember) {
return promise.make(function(resolve, reject) {
promise.wait(api.post('/login', {userNameOrEmail: userNameOrEmail, password: password}))
.then(function(response) {
updateAppState(response);
jQuery.cookie(
'auth',
response.json.token.name,
remember ? { expires: 365 } : {});
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
function loginFromCredentials(userNameOrEmail, password, remember) {
return promise.make(function(resolve, reject) {
promise.wait(api.post('/login', {userNameOrEmail: userNameOrEmail, password: password}))
.then(function(response) {
updateAppState(response);
jQuery.cookie(
'auth',
response.json.token.name,
remember ? { expires: 365 } : {});
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
function loginFromToken(token, isFromCookie) {
return promise.make(function(resolve, reject) {
var fd = {
token: token,
isFromCookie: isFromCookie
};
promise.wait(api.post('/login', fd))
.then(function(response) {
updateAppState(response);
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
function loginFromToken(token, isFromCookie) {
return promise.make(function(resolve, reject) {
var fd = {
token: token,
isFromCookie: isFromCookie
};
promise.wait(api.post('/login', fd))
.then(function(response) {
updateAppState(response);
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
function loginAnonymous() {
return promise.make(function(resolve, reject) {
promise.wait(api.post('/login'))
.then(function(response) {
updateAppState(response);
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
function loginAnonymous() {
return promise.make(function(resolve, reject) {
promise.wait(api.post('/login'))
.then(function(response) {
updateAppState(response);
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
function logout() {
return promise.make(function(resolve, reject) {
jQuery.removeCookie('auth');
appState.set('loginToken', null);
return promise.wait(loginAnonymous())
.then(resolve)
.fail(reject);
});
}
function logout() {
return promise.make(function(resolve, reject) {
jQuery.removeCookie('auth');
appState.set('loginToken', null);
return promise.wait(loginAnonymous())
.then(resolve)
.fail(reject);
});
}
function tryLoginFromCookie() {
return promise.make(function(resolve, reject) {
if (isLoggedIn()) {
resolve();
return;
}
function tryLoginFromCookie() {
return promise.make(function(resolve, reject) {
if (isLoggedIn()) {
resolve();
return;
}
var authCookie = jQuery.cookie('auth');
if (!authCookie) {
reject();
return;
}
var authCookie = jQuery.cookie('auth');
if (!authCookie) {
reject();
return;
}
promise.wait(loginFromToken(authCookie, true))
.then(function(response) {
resolve();
}).fail(function(response) {
jQuery.removeCookie('auth');
reject();
});
});
}
promise.wait(loginFromToken(authCookie, true))
.then(function(response) {
resolve();
}).fail(function(response) {
jQuery.removeCookie('auth');
reject();
});
});
}
function updateAppState(response) {
appState.set('privileges', response.json.privileges || []);
appState.set('loginToken', response.json.token && response.json.token.name);
appState.set('loggedIn', response.json.user && !!response.json.user.id);
appState.set('loggedInUser', response.json.user);
}
function updateAppState(response) {
appState.set('privileges', response.json.privileges || []);
appState.set('loginToken', response.json.token && response.json.token.name);
appState.set('loggedIn', response.json.user && !!response.json.user.id);
appState.set('loggedInUser', response.json.user);
appState.set('config', response.json.config);
}
function isLoggedIn(userName) {
if (!appState.get('loggedIn')) {
return false;
}
if (typeof(userName) !== 'undefined') {
if (getCurrentUser().name !== userName) {
return false;
}
}
return true;
}
function isLoggedIn(userName) {
if (!appState.get('loggedIn')) {
return false;
}
if (typeof(userName) !== 'undefined') {
if (getCurrentUser().name !== userName) {
return false;
}
}
return true;
}
function getCurrentUser() {
return appState.get('loggedInUser');
}
function getCurrentUser() {
return appState.get('loggedInUser');
}
function getCurrentPrivileges() {
return appState.get('privileges');
}
function getCurrentPrivileges() {
return appState.get('privileges');
}
function updateCurrentUser(user) {
if (user.id !== getCurrentUser().id) {
throw new Error('Cannot set current user to other user this way.');
}
appState.set('loggedInUser', user);
}
function updateCurrentUser(user) {
if (user.id !== getCurrentUser().id) {
throw new Error('Cannot set current user to other user this way.');
}
appState.set('loggedInUser', user);
}
function hasPrivilege(privilege) {
return _.contains(getCurrentPrivileges(), privilege);
}
function hasPrivilege(privilege) {
return _.contains(getCurrentPrivileges(), privilege);
}
function startObservingLoginChanges(listenerName, callback) {
appState.startObserving('loggedInUser', listenerName, callback);
}
function startObservingLoginChanges(listenerName, callback) {
appState.startObserving('loggedInUser', listenerName, callback);
}
return {
loginFromCredentials: loginFromCredentials,
loginFromToken: loginFromToken,
loginAnonymous: loginAnonymous,
tryLoginFromCookie: tryLoginFromCookie,
logout: logout,
return {
loginFromCredentials: loginFromCredentials,
loginFromToken: loginFromToken,
loginAnonymous: loginAnonymous,
tryLoginFromCookie: tryLoginFromCookie,
logout: logout,
startObservingLoginChanges: startObservingLoginChanges,
isLoggedIn: isLoggedIn,
getCurrentUser: getCurrentUser,
updateCurrentUser: updateCurrentUser,
getCurrentPrivileges: getCurrentPrivileges,
hasPrivilege: hasPrivilege,
startObservingLoginChanges: startObservingLoginChanges,
isLoggedIn: isLoggedIn,
getCurrentUser: getCurrentUser,
updateCurrentUser: updateCurrentUser,
getCurrentPrivileges: getCurrentPrivileges,
hasPrivilege: hasPrivilege,
privileges: privileges,
};
privileges: privileges,
};
};

View File

@ -2,29 +2,29 @@ var App = App || {};
App.Bootstrap = function(auth, router, promise, presenterManager) {
promise.wait(presenterManager.init())
.then(function() {
promise.wait(auth.tryLoginFromCookie())
.then(startRouting)
.fail(function(error) {
promise.wait(auth.loginAnonymous())
.then(startRouting)
.fail(function() {
console.log(arguments);
window.alert('Fatal authentication error');
});
});
}).fail(function() {
console.log(arguments);
});
promise.wait(presenterManager.init())
.then(function() {
promise.wait(auth.tryLoginFromCookie())
.then(startRouting)
.fail(function(error) {
promise.wait(auth.loginAnonymous())
.then(startRouting)
.fail(function() {
console.log(arguments);
window.alert('Fatal authentication error');
});
});
}).fail(function() {
console.log(arguments);
});
function startRouting() {
try {
router.start();
} catch (err) {
console.log(err);
}
}
function startRouting() {
try {
router.start();
} catch (err) {
console.log(err);
}
}
};

View File

@ -1,96 +1,98 @@
var App = App || {};
App.BrowsingSettings = function(
promise,
auth,
api) {
promise,
auth,
api) {
var settings = getDefaultSettings();
var settings = getDefaultSettings();
auth.startObservingLoginChanges('browsing-settings', loginStateChanged);
auth.startObservingLoginChanges('browsing-settings', loginStateChanged);
readFromLocalStorage();
if (auth.isLoggedIn()) {
loginStateChanged();
}
readFromLocalStorage();
if (auth.isLoggedIn()) {
loginStateChanged();
}
function setSettings(newSettings) {
settings = newSettings;
return save();
}
function setSettings(newSettings) {
settings = newSettings;
return save();
}
function getSettings() {
return settings;
}
function getSettings() {
return settings;
}
function getDefaultSettings() {
return {
hideDownvoted: true,
endlessScroll: false,
listPosts: {
safe: true,
sketchy: true,
unsafe: true,
},
};
}
function getDefaultSettings() {
return {
hideDownvoted: true,
endlessScroll: false,
listPosts: {
safe: true,
sketchy: true,
unsafe: true,
},
keyboardShortcuts: true,
fitMode: 'fit-width',
upscale: false,
};
}
function loginStateChanged() {
readFromUser(auth.getCurrentUser());
}
function loginStateChanged() {
readFromUser(auth.getCurrentUser());
}
function readFromLocalStorage() {
readFromString(localStorage.getItem('browsingSettings'));
}
function readFromLocalStorage() {
readFromString(localStorage.getItem('browsingSettings'));
}
function readFromUser(user) {
readFromString(user.browsingSettings);
}
function readFromUser(user) {
readFromString(user.browsingSettings);
}
function readFromString(string) {
if (!string) {
return;
}
function readFromString(string) {
if (!string) {
return;
}
try {
if (typeof(string) === 'string' || string instanceof String) {
settings = JSON.parse(string);
} else {
settings = string;
}
} catch (e) {
}
}
try {
if (typeof(string) === 'string' || string instanceof String) {
settings = JSON.parse(string);
} else {
settings = string;
}
} catch (e) {
}
}
function saveToLocalStorage() {
localStorage.setItem('browsingSettings', JSON.stringify(settings));
}
function saveToLocalStorage() {
localStorage.setItem('browsingSettings', JSON.stringify(settings));
}
function saveToUser(user) {
var formData = {
browsingSettings: JSON.stringify(settings),
};
return api.put('/users/' + user.name, formData);
}
function saveToUser(user) {
var formData = {
browsingSettings: JSON.stringify(settings),
};
return api.post('/users/' + user.name, formData);
}
function save() {
return promise.make(function(resolve, reject) {
saveToLocalStorage();
if (auth.isLoggedIn()) {
promise.wait(saveToUser(auth.getCurrentUser()))
.then(resolve)
.fail(reject);
} else {
resolve();
}
});
}
return {
getSettings: getSettings,
setSettings: setSettings,
};
function save() {
return promise.make(function(resolve, reject) {
saveToLocalStorage();
if (auth.isLoggedIn()) {
promise.wait(saveToUser(auth.getCurrentUser()))
.then(resolve)
.fail(reject);
} else {
resolve();
}
});
}
return {
getSettings: getSettings,
setSettings: setSettings,
};
};
App.DI.registerSingleton('browsingSettings', ['promise', 'auth', 'api'], App.BrowsingSettings);

View File

@ -2,240 +2,273 @@ var App = App || {};
App.Controls = App.Controls || {};
App.Controls.AutoCompleteInput = function($input) {
var _ = App.DI.get('_');
var jQuery = App.DI.get('jQuery');
var tagList = App.DI.get('tagList');
var _ = App.DI.get('_');
var jQuery = App.DI.get('jQuery');
var tagList = App.DI.get('tagList');
var KEY_RETURN = 13;
var KEY_ESCAPE = 27;
var KEY_UP = 38;
var KEY_DOWN = 40;
var KEY_TAB = 9;
var KEY_RETURN = 13;
var KEY_DELETE = 46;
var KEY_ESCAPE = 27;
var KEY_UP = 38;
var KEY_DOWN = 40;
var options = {
caseSensitive: false,
source: null,
maxResults: 15,
minLengthToArbitrarySearch: 3,
onApply: null,
additionalFilter: null,
};
var showTimeout = null;
var cachedSource = null;
var results = [];
var activeResult = -1;
var options = {
caseSensitive: false,
source: null,
maxResults: 15,
minLengthToArbitrarySearch: 3,
onApply: null,
onDelete: null,
onRender: null,
additionalFilter: null,
};
var showTimeout = null;
var cachedSource = null;
var results = [];
var activeResult = -1;
var monitorInputHidingInterval = null;
if ($input.length === 0) {
throw new Error('Input element was not found');
}
if ($input.length > 1) {
throw new Error('Cannot add autocompletion to more than one element at once');
}
if ($input.attr('data-autocomplete')) {
throw new Error('Autocompletion was already added for this element');
}
$input.attr('data-autocomplete', true);
$input.attr('autocomplete', 'off');
if ($input.length === 0) {
throw new Error('Input element was not found');
}
if ($input.length > 1) {
throw new Error('Cannot add autocompletion to more than one element at once');
}
if ($input.attr('data-autocomplete')) {
throw new Error('Autocompletion was already added for this element');
}
$input.attr('data-autocomplete', true);
$input.attr('autocomplete', 'off');
var $div = jQuery('<div>');
var $list = jQuery('<ul>');
$div.addClass('autocomplete');
$div.append($list);
jQuery(document.body).append($div);
var $div = jQuery('<div>');
var $list = jQuery('<ul>');
$div.addClass('autocomplete');
$div.append($list);
jQuery(document.body).append($div);
function getSource() {
if (cachedSource) {
return cachedSource;
} else {
var source = tagList.getTags();
source = _.sortBy(source, function(a) { return -a.usages; });
source = _.filter(source, function(a) { return a.usages >= 0; });
source = _.map(source, function(a) {
return {
tag: a.name,
caption: a.name + ' (' + a.usages + ')',
};
});
cachedSource = source;
return source;
}
}
function getSource() {
if (cachedSource) {
return cachedSource;
} else {
var source = tagList.getTags();
source = _.sortBy(source, function(a) { return -a.usages; });
source = _.filter(source, function(a) { return a.usages >= 0; });
source = _.map(source, function(a) {
return {
tag: a.name,
caption: a.name + ' (' + a.usages + ')',
};
});
cachedSource = source;
return source;
}
}
$input.bind('keydown', function(e) {
if (isShown() && e.which === KEY_ESCAPE) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
hide();
} else if (isShown() && e.which === KEY_DOWN) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
selectNext();
} else if (isShown() && e.which === KEY_UP) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
selectPrevious();
} else if (isShown() && e.which === KEY_RETURN && activeResult >= 0) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
applyAutocomplete();
hide();
} else {
window.clearTimeout(showTimeout);
showTimeout = window.setTimeout(showOrHide, 250);
}
});
$input.bind('keydown', function(e) {
var func = null;
if (isShown() && e.which === KEY_ESCAPE) {
func = hide;
} else if (isShown() && e.which === KEY_TAB) {
if (e.shiftKey) {
func = selectPrevious;
} else {
func = selectNext;
}
} else if (isShown() && e.which === KEY_DOWN) {
func = selectNext;
} else if (isShown() && e.which === KEY_UP) {
func = selectPrevious;
} else if (isShown() && e.which === KEY_RETURN && activeResult >= 0) {
func = function() { applyAutocomplete(); hide(); };
} else if (isShown() && e.which === KEY_DELETE && activeResult >= 0) {
func = function() { applyDelete(); hide(); };
}
$input.blur(function(e) {
window.clearTimeout(showTimeout);
window.setTimeout(function() { hide(); }, 50);
});
if (func !== null) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
func();
} else {
window.clearTimeout(showTimeout);
showTimeout = window.setTimeout(showOrHide, 250);
}
});
function getSelectionStart(){
var input = $input.get(0);
if (!input) {
return;
}
if ('selectionStart' in input) {
return input.selectionStart;
} else if (document.selection) {
input.focus();
var sel = document.selection.createRange();
var selLen = document.selection.createRange().text.length;
sel.moveStart('character', -input.value.length);
return sel.text.length - selLen;
} else {
return 0;
}
}
$input.blur(function(e) {
window.clearTimeout(showTimeout);
window.setTimeout(function() { hide(); }, 50);
});
function getTextToFind() {
var val = $input.val();
var start = getSelectionStart();
return val.substring(0, start).replace(/.*\s/, '');
}
function getSelectionStart(){
var input = $input.get(0);
if (!input) {
return;
}
if ('selectionStart' in input) {
return input.selectionStart;
} else if (document.selection) {
input.focus();
var sel = document.selection.createRange();
var selLen = document.selection.createRange().text.length;
sel.moveStart('character', -input.value.length);
return sel.text.length - selLen;
} else {
return 0;
}
}
function showOrHide() {
var textToFind = getTextToFind();
if (textToFind.length === 0) {
hide();
} else {
updateResults(textToFind);
refreshList();
}
}
function getTextToFind() {
var val = $input.val();
var start = getSelectionStart();
return val.substring(0, start).replace(/.*\s/, '');
}
function isShown() {
return $div.is(':visible');
}
function showOrHide() {
var textToFind = getTextToFind();
if (textToFind.length === 0) {
hide();
} else {
updateResults(textToFind);
refreshList();
}
}
function hide() {
$div.hide();
}
function isShown() {
return $div.is(':visible');
}
function selectPrevious() {
select(activeResult === -1 ? results.length - 1 : activeResult - 1);
}
function hide() {
$div.hide();
window.clearInterval(monitorInputHidingInterval);
}
function selectNext() {
select(activeResult === -1 ? 0 : activeResult + 1);
}
function selectPrevious() {
select(activeResult === -1 ? results.length - 1 : activeResult - 1);
}
function select(newActiveResult) {
if (newActiveResult >= 0 && newActiveResult < results.length) {
activeResult = newActiveResult;
refreshActiveResult();
} else {
activeResult = - 1;
refreshActiveResult();
}
}
function selectNext() {
select(activeResult === -1 ? 0 : activeResult + 1);
}
function getResultsFilter(textToFind) {
if (textToFind.length < options.minLengthToArbitrarySearch) {
return options.caseSensitive ?
function(resultItem) { return resultItem.tag.indexOf(textToFind) === 0; } :
function(resultItem) { return resultItem.tag.toLowerCase().indexOf(textToFind.toLowerCase()) === 0; };
} else {
return options.caseSensitive ?
function(resultItem) { return resultItem.tag.indexOf(textToFind) >= 0; } :
function(resultItem) { return resultItem.tag.toLowerCase().indexOf(textToFind.toLowerCase()) >= 0; };
}
}
function select(newActiveResult) {
if (newActiveResult >= 0 && newActiveResult < results.length) {
activeResult = newActiveResult;
refreshActiveResult();
} else {
activeResult = - 1;
refreshActiveResult();
}
}
function updateResults(textToFind) {
var oldResults = results.slice();
var source = getSource();
var filter = getResultsFilter(textToFind);
results = _.filter(source, filter);
if (options.additionalFilter) {
results = options.additionalFilter(results);
}
results = results.slice(0, options.maxResults);
if (!_.isEqual(oldResults, results)) {
activeResult = -1;
}
}
function getResultsFilter(textToFind) {
if (textToFind.length < options.minLengthToArbitrarySearch) {
return options.caseSensitive ?
function(resultItem) { return resultItem.tag.indexOf(textToFind) === 0; } :
function(resultItem) { return resultItem.tag.toLowerCase().indexOf(textToFind.toLowerCase()) === 0; };
} else {
return options.caseSensitive ?
function(resultItem) { return resultItem.tag.indexOf(textToFind) >= 0; } :
function(resultItem) { return resultItem.tag.toLowerCase().indexOf(textToFind.toLowerCase()) >= 0; };
}
}
function applyAutocomplete() {
if (options.onApply) {
options.onApply(results[activeResult].tag);
} else {
var val = $input.val();
var start = getSelectionStart();
var prefix = '';
var suffix = val.substring(start);
var middle = val.substring(0, start);
var index = middle.lastIndexOf(' ');
if (index !== -1) {
prefix = val.substring(0, index + 1);
middle = val.substring(index + 1);
}
$input.val(prefix + results[activeResult].tag + ' ' + suffix.trimLeft());
$input.focus();
}
}
function updateResults(textToFind) {
var oldResults = results.slice();
var source = getSource();
var filter = getResultsFilter(textToFind);
results = _.filter(source, filter);
if (options.additionalFilter) {
results = options.additionalFilter(results);
}
results = results.slice(0, options.maxResults);
if (!_.isEqual(oldResults, results)) {
activeResult = -1;
}
}
function refreshList() {
if (results.length === 0) {
hide();
return;
}
function applyDelete() {
if (options.onDelete) {
options.onDelete(results[activeResult].tag);
}
}
$list.empty();
_.each(results, function(resultItem, resultIndex) {
var $listItem = jQuery('<li/>');
$listItem.text(resultItem.caption);
$listItem.attr('data-key', resultItem.tag);
$listItem.hover(function(e) {
e.preventDefault();
activeResult = resultIndex;
refreshActiveResult();
});
$listItem.mousedown(function(e) {
e.preventDefault();
activeResult = resultIndex;
applyAutocomplete();
hide();
});
$list.append($listItem);
});
refreshActiveResult();
$div.css({
left: ($input.offset().left) + 'px',
top: ($input.offset().top + $input.outerHeight() - 2) + 'px',
});
$div.show();
}
function applyAutocomplete() {
if (options.onApply) {
options.onApply(results[activeResult].tag);
} else {
var val = $input.val();
var start = getSelectionStart();
var prefix = '';
var suffix = val.substring(start);
var middle = val.substring(0, start);
var index = middle.lastIndexOf(' ');
if (index !== -1) {
prefix = val.substring(0, index + 1);
middle = val.substring(index + 1);
}
$input.val(prefix + results[activeResult].tag + ' ' + suffix.trimLeft());
$input.focus();
}
}
function refreshActiveResult() {
$list.find('li.active').removeClass('active');
if (activeResult >= 0) {
$list.find('li').eq(activeResult).addClass('active');
}
}
function refreshList() {
if (results.length === 0) {
hide();
return;
}
return options;
$list.empty();
_.each(results, function(resultItem, resultIndex) {
var $listItem = jQuery('<li/>');
$listItem.text(resultItem.caption);
$listItem.attr('data-key', resultItem.tag);
$listItem.hover(function(e) {
e.preventDefault();
activeResult = resultIndex;
refreshActiveResult();
});
$listItem.mousedown(function(e) {
e.preventDefault();
activeResult = resultIndex;
applyAutocomplete();
hide();
});
$list.append($listItem);
});
if (options.onRender) {
options.onRender($list);
}
refreshActiveResult();
var x = $input.offset().left;
var y = $input.offset().top + $input.outerHeight() - 2;
if (y + $div.height() > window.innerHeight) {
y = $input.offset().top - $div.height();
}
$div.css({
left: x + 'px',
top: y + 'px',
});
$div.show();
monitorInputHiding();
}
function refreshActiveResult() {
$list.find('li.active').removeClass('active');
if (activeResult >= 0) {
$list.find('li').eq(activeResult).addClass('active');
}
}
function monitorInputHiding() {
monitorInputHidingInterval = window.setInterval(function() {
if (!$input.is(':visible')) {
hide();
}
}, 100);
}
return options;
};

View File

@ -2,64 +2,64 @@ var App = App || {};
App.Controls = App.Controls || {};
App.Controls.FileDropper = function($fileInput) {
var _ = App.DI.get('_');
var jQuery = App.DI.get('jQuery');
var _ = App.DI.get('_');
var jQuery = App.DI.get('jQuery');
var options = {
onChange: null,
setNames: false,
};
var options = {
onChange: null,
setNames: false,
};
var $dropDiv = jQuery('<button type="button" class="file-handler"></button>');
var allowMultiple = $fileInput.attr('multiple');
$dropDiv.html((allowMultiple ? 'Drop files here!' : 'Drop file here!') + '<br/>Or just click on this box.');
$dropDiv.insertBefore($fileInput);
$fileInput.attr('multiple', allowMultiple);
$fileInput.hide();
var $dropDiv = jQuery('<button type="button" class="file-handler"></button>');
var allowMultiple = $fileInput.attr('multiple');
$dropDiv.html((allowMultiple ? 'Drop files here!' : 'Drop file here!') + '<br/>Or just click on this box.');
$dropDiv.insertBefore($fileInput);
$fileInput.attr('multiple', allowMultiple);
$fileInput.hide();
$fileInput.change(function(e) {
addFiles(this.files);
});
$fileInput.change(function(e) {
addFiles(this.files);
});
$dropDiv.on('dragenter', function(e) {
$dropDiv.addClass('active');
}).on('dragleave', function(e) {
$dropDiv.removeClass('active');
}).on('dragover', function(e) {
e.preventDefault();
}).on('drop', function(e) {
e.preventDefault();
addFiles(e.originalEvent.dataTransfer.files);
}).on('click', function(e) {
$fileInput.show().focus().trigger('click').hide();
$dropDiv.addClass('active');
});
$dropDiv.on('dragenter', function(e) {
$dropDiv.addClass('active');
}).on('dragleave', function(e) {
$dropDiv.removeClass('active');
}).on('dragover', function(e) {
e.preventDefault();
}).on('drop', function(e) {
e.preventDefault();
addFiles(e.originalEvent.dataTransfer.files);
}).on('click', function(e) {
$fileInput.show().focus().trigger('click').hide();
$dropDiv.addClass('active');
});
function addFiles(files) {
$dropDiv.removeClass('active');
if (!allowMultiple && files.length > 1) {
window.alert('Cannot select multiple files.');
return;
}
if (typeof(options.onChange) !== 'undefined') {
options.onChange(files);
}
if (options.setNames && !allowMultiple) {
$dropDiv.text(files[0].name);
}
}
function addFiles(files) {
$dropDiv.removeClass('active');
if (!allowMultiple && files.length > 1) {
window.alert('Cannot select multiple files.');
return;
}
if (typeof(options.onChange) !== 'undefined') {
options.onChange(files);
}
if (options.setNames && !allowMultiple) {
$dropDiv.text(files[0].name);
}
}
function readAsDataURL(file, callback) {
var reader = new FileReader();
reader.onloadend = function() {
callback(reader.result);
};
reader.readAsDataURL(file);
}
function readAsDataURL(file, callback) {
var reader = new FileReader();
reader.onloadend = function() {
callback(reader.result);
};
reader.readAsDataURL(file);
}
_.extend(options, {
readAsDataURL: readAsDataURL,
});
_.extend(options, {
readAsDataURL: readAsDataURL,
});
return options;
return options;
};

View File

@ -2,377 +2,419 @@ var App = App || {};
App.Controls = App.Controls || {};
App.Controls.TagInput = function($underlyingInput) {
var _ = App.DI.get('_');
var jQuery = App.DI.get('jQuery');
var promise = App.DI.get('promise');
var api = App.DI.get('api');
var tagList = App.DI.get('tagList');
var _ = App.DI.get('_');
var jQuery = App.DI.get('jQuery');
var promise = App.DI.get('promise');
var api = App.DI.get('api');
var tagList = App.DI.get('tagList');
var KEY_RETURN = 13;
var KEY_SPACE = 32;
var KEY_BACKSPACE = 8;
var tagConfirmKeys = [KEY_RETURN, KEY_SPACE];
var inputConfirmKeys = [KEY_RETURN];
var KEY_RETURN = 13;
var KEY_SPACE = 32;
var KEY_BACKSPACE = 8;
var tagConfirmKeys = [KEY_RETURN, KEY_SPACE];
var inputConfirmKeys = [KEY_RETURN];
var tags = [];
var options = {
beforeTagAdded: null,
beforeTagRemoved: null,
inputConfirmed: null,
};
var SOURCE_INITIAL_TEXT = 1;
var SOURCE_AUTOCOMPLETION = 2;
var SOURCE_PASTE = 3;
var SOURCE_IMPLICATIONS = 4;
var SOURCE_INPUT_BLUR = 5;
var SOURCE_INPUT_ENTER = 6;
var SOURCE_SUGGESTIONS = 7;
var $wrapper = jQuery('<div class="tag-input">');
var $tagList = jQuery('<ul class="tags">');
var $input = jQuery('<input class="tag-real-input" type="text"/>');
var $siblings = jQuery('<div class="related-tags"><span>Sibling tags:</span><ul>');
var $suggestions = jQuery('<div class="related-tags"><span>Suggested tags:</span><ul>');
init();
render();
initAutoComplete();
var tags = [];
var options = {
beforeTagAdded: null,
beforeTagRemoved: null,
inputConfirmed: null,
};
function init() {
if ($underlyingInput.length === 0) {
throw new Error('Tag input element was not found');
}
if ($underlyingInput.length > 1) {
throw new Error('Cannot set tag input to more than one element at once');
}
if ($underlyingInput.attr('data-tagged')) {
throw new Error('Tag input was already initialized for this element');
}
$underlyingInput.attr('data-tagged', true);
}
var $wrapper = jQuery('<div class="tag-input">');
var $tagList = jQuery('<ul class="tags">');
var tagInputId = 'tags' + Math.random();
var $label = jQuery('<label for="' + tagInputId + '" style="display: none">Tags:</label>');
var $input = jQuery('<input class="tag-real-input" type="text" id="' + tagInputId + '"/>');
var $siblings = jQuery('<div class="related-tags"><span>Sibling tags:</span><ul>');
var $suggestions = jQuery('<div class="related-tags"><span>Suggested tags:</span><ul>');
init();
render();
initAutoComplete();
function render() {
$underlyingInput.hide();
$wrapper.append($tagList);
$wrapper.append($input);
$wrapper.insertAfter($underlyingInput);
$wrapper.click(function(e) {
if (e.target.nodeName === 'LI') {
return;
}
e.preventDefault();
$input.focus();
});
$input.attr('placeholder', $underlyingInput.attr('placeholder'));
$suggestions.insertAfter($wrapper);
$siblings.insertAfter($wrapper);
function init() {
if ($underlyingInput.length === 0) {
throw new Error('Tag input element was not found');
}
if ($underlyingInput.length > 1) {
throw new Error('Cannot set tag input to more than one element at once');
}
if ($underlyingInput.attr('data-tagged')) {
throw new Error('Tag input was already initialized for this element');
}
$underlyingInput.attr('data-tagged', true);
}
processText($underlyingInput.val(), addTagDirectly);
function render() {
$underlyingInput.hide();
$wrapper.append($tagList);
$wrapper.append($label);
$wrapper.append($input);
$wrapper.insertAfter($underlyingInput);
$wrapper.click(function(e) {
if (e.target.nodeName === 'LI') {
return;
}
e.preventDefault();
$input.focus();
});
$input.attr('placeholder', $underlyingInput.attr('placeholder'));
$siblings.insertAfter($wrapper);
$suggestions.insertAfter($wrapper);
$underlyingInput.val('');
}
processText($underlyingInput.val(), SOURCE_INITIAL_TEXT);
function initAutoComplete() {
var autoComplete = new App.Controls.AutoCompleteInput($input);
autoComplete.onApply = function(text) {
processText(text, addTag);
$input.val('');
};
autoComplete.additionalFilter = function(results) {
return _.filter(results, function(resultItem) {
return !_.contains(getTags(), resultItem[0]);
});
};
}
$underlyingInput.val('');
}
$input.bind('focus', function(e) {
$wrapper.addClass('focused');
});
$input.bind('blur', function(e) {
$wrapper.removeClass('focused');
var tagName = $input.val();
addTag(tagName);
$input.val('');
});
function initAutoComplete() {
var autoComplete = new App.Controls.AutoCompleteInput($input);
autoComplete.onDelete = function(text) {
removeTag(text);
$input.val('');
};
autoComplete.onApply = function(text) {
processText(text, SOURCE_AUTOCOMPLETION);
$input.val('');
};
autoComplete.additionalFilter = function(results) {
return _.filter(results, function(resultItem) {
return !_.contains(getTags(), resultItem[0]);
});
};
autoComplete.onRender = function($list) {
$list.find('li').each(function() {
var $li = jQuery(this);
if (isTaggedWith($li.attr('data-key'))) {
$li.css('opacity', '0.5');
}
});
};
}
$input.bind('paste', function(e) {
e.preventDefault();
var pastedText;
if (window.clipboardData) {
pastedText = window.clipboardData.getData('Text');
} else {
pastedText = (e.originalEvent || e).clipboardData.getData('text/plain');
}
$input.bind('focus', function(e) {
$wrapper.addClass('focused');
});
$input.bind('blur', function(e) {
$wrapper.removeClass('focused');
var tagName = $input.val();
addTag(tagName, SOURCE_INPUT_BLUR);
$input.val('');
});
if (pastedText.length > 200) {
window.alert('Pasted text is too long.');
return;
}
$input.bind('paste', function(e) {
e.preventDefault();
var pastedText;
if (window.clipboardData) {
pastedText = window.clipboardData.getData('Text');
} else {
pastedText = (e.originalEvent || e).clipboardData.getData('text/plain');
}
processTextWithoutLast(pastedText, addTag);
});
if (pastedText.length > 2000) {
window.alert('Pasted text is too long.');
return;
}
$input.bind('keydown', function(e) {
if (_.contains(inputConfirmKeys, e.which) && !$input.val()) {
e.preventDefault();
if (typeof(options.inputConfirmed) !== 'undefined') {
options.inputConfirmed();
}
} else if (_.contains(tagConfirmKeys, e.which)) {
var tagName = $input.val();
e.preventDefault();
$input.val('');
addTag(tagName);
} else if (e.which === KEY_BACKSPACE && jQuery(this).val().length === 0) {
e.preventDefault();
removeLastTag();
}
});
processTextWithoutLast(pastedText, SOURCE_PASTE);
});
function explodeText(text) {
return _.filter(text.trim().split(/\s+/), function(item) {
return item.length > 0;
});
}
$input.bind('keydown', function(e) {
if (_.contains(inputConfirmKeys, e.which) && !$input.val()) {
e.preventDefault();
if (typeof(options.inputConfirmed) !== 'undefined') {
options.inputConfirmed();
}
} else if (_.contains(tagConfirmKeys, e.which)) {
var tagName = $input.val();
e.preventDefault();
$input.val('');
addTag(tagName, SOURCE_INPUT_ENTER);
} else if (e.which === KEY_BACKSPACE && jQuery(this).val().length === 0) {
e.preventDefault();
removeLastTag();
}
});
function processText(text, callback) {
var tagNamesToAdd = explodeText(text);
_.map(tagNamesToAdd, function(tagName) { callback(tagName); });
}
function explodeText(text) {
return _.filter(text.trim().split(/\s+/), function(item) {
return item.length > 0;
});
}
function processTextWithoutLast(text, callback) {
var tagNamesToAdd = explodeText(text);
var lastTagName = tagNamesToAdd.pop();
_.map(tagNamesToAdd, function(tagName) { callback(tagName); });
$input.val(lastTagName);
}
function processText(text, source) {
var tagNamesToAdd = explodeText(text);
_.map(tagNamesToAdd, function(tagName) { addTag(tagName, source); });
}
function addTag(tagName) {
tagName = tagName.trim();
if (tagName.length === 0) {
return;
}
function processTextWithoutLast(text, source) {
var tagNamesToAdd = explodeText(text);
var lastTagName = tagNamesToAdd.pop();
_.map(tagNamesToAdd, function(tagName) { addTag(tagName, source); });
$input.val(lastTagName);
}
if (tagName.length > 64) {
//showing alert inside keydown event leads to mysterious behaviors
//in some browsers, hence the timeout
window.setTimeout(function() {
window.alert('Tag is too long.');
}, 10);
return;
}
function addTag(tagName, source) {
tagName = tagName.trim();
if (tagName.length === 0) {
return;
}
if (isTaggedWith(tagName)) {
flashTagRed(tagName);
} else {
beforeTagAdded(tagName);
if (tagName.length > 64) {
//showing alert inside keydown event leads to mysterious behaviors
//in some browsers, hence the timeout
window.setTimeout(function() {
window.alert('Tag is too long.');
}, 10);
return;
}
var exportedTag = getExportedTag(tagName);
if (!exportedTag || !exportedTag.banned) {
addTagDirectly(tagName);
}
if (isTaggedWith(tagName)) {
flashTagRed(tagName);
} else {
beforeTagAdded(tagName, source);
afterTagAdded(tagName);
}
}
var exportedTag = getExportedTag(tagName);
if (!exportedTag || !exportedTag.banned) {
tags.push(tagName);
var $elem = createListElement(tagName);
$tagList.append($elem);
}
function addTagDirectly(tagName) {
tags.push(tagName);
var $elem = createListElement(tagName);
$tagList.append($elem);
}
afterTagAdded(tagName, source);
}
}
function beforeTagAdded(tagName) {
if (typeof(options.beforeTagAdded) === 'function') {
options.beforeTagAdded(tagName);
}
}
function beforeTagRemoved(tagName) {
if (typeof(options.beforeTagRemoved) === 'function') {
options.beforeTagRemoved(tagName);
}
}
function afterTagAdded(tagName) {
var tag = getExportedTag(tagName);
if (tag) {
_.each(tag.implications, function(impliedTagName) {
addTag(impliedTagName);
flashTagYellow(impliedTagName);
});
showOrHideSuggestions(tag.suggestions);
} else {
flashTagGreen(tagName);
}
}
function afterTagRemoved(tagName) {
refreshShownSiblings();
}
function getExportedTag(tagName) {
return _.first(_.filter(
tagList.getTags(),
function(t) {
return t.name.toLowerCase() === tagName.toLowerCase();
}));
}
function beforeTagAdded(tagName, source) {
if (typeof(options.beforeTagAdded) === 'function') {
options.beforeTagAdded(tagName);
}
}
function removeTag(tagName) {
var oldTagNames = getTags();
var newTagNames = _.without(oldTagNames, tagName);
if (newTagNames.length !== oldTagNames.length) {
if (typeof(options.beforeTagRemoved) === 'function') {
options.beforeTagRemoved(tagName);
}
setTags(newTagNames);
}
}
function afterTagAdded(tagName, source) {
if (source === SOURCE_IMPLICATIONS) {
flashTagYellow(tagName);
} else if (source !== SOURCE_INITIAL_TEXT) {
var tag = getExportedTag(tagName);
if (tag) {
_.each(tag.implications, function(impliedTagName) {
if (!isTaggedWith(impliedTagName)) {
addTag(impliedTagName, SOURCE_IMPLICATIONS);
}
});
if (source !== SOURCE_IMPLICATIONS && source !== SOURCE_SUGGESTIONS) {
showOrHideSuggestions(tagName);
refreshShownSiblings();
}
} else {
flashTagGreen(tagName);
}
}
}
function isTaggedWith(tagName) {
var tagNames = _.map(getTags(), function(tagName) {
return tagName.toLowerCase();
});
return _.contains(tagNames, tagName.toLowerCase());
}
function getExportedTag(tagName) {
return _.first(_.filter(
tagList.getTags(),
function(t) {
return t.name.toLowerCase() === tagName.toLowerCase();
}));
}
function removeLastTag() {
removeTag(_.last(getTags()));
}
function removeTag(tagName) {
var oldTagNames = getTags();
var newTagNames = _.without(oldTagNames, tagName);
if (newTagNames.length !== oldTagNames.length) {
beforeTagRemoved(tagName);
setTags(newTagNames);
afterTagRemoved(tagName);
}
}
function flashTagRed(tagName) {
flashTag(tagName, 'rgba(255, 200, 200, 1)');
}
function isTaggedWith(tagName) {
var tagNames = _.map(getTags(), function(tagName) {
return tagName.toLowerCase();
});
return _.contains(tagNames, tagName.toLowerCase());
}
function flashTagYellow(tagName) {
flashTag(tagName, 'rgba(255, 255, 200, 1)');
}
function removeLastTag() {
removeTag(_.last(getTags()));
}
function flashTagGreen(tagName) {
flashTag(tagName, 'rgba(200, 255, 200, 1)');
}
function flashTagRed(tagName) {
flashTag(tagName, 'rgba(255, 200, 200, 1)');
}
function flashTag(tagName, color) {
var $elem = getListElement(tagName);
$elem.css({backgroundColor: color});
}
function flashTagYellow(tagName) {
flashTag(tagName, 'rgba(255, 255, 200, 1)');
}
function getListElement(tagName) {
return $tagList.find('li[data-tag="' + tagName.toLowerCase() + '"]');
}
function flashTagGreen(tagName) {
flashTag(tagName, 'rgba(200, 255, 200, 1)');
}
function setTags(newTagNames) {
tags = newTagNames.slice();
$tagList.empty();
$underlyingInput.val(newTagNames.join(' '));
_.each(newTagNames, function(tagName) {
var $elem = createListElement(tagName);
$tagList.append($elem);
});
}
function flashTag(tagName, color) {
var $elem = getListElement(tagName);
$elem.css({backgroundColor: color});
}
function createListElement(tagName) {
var $elem = jQuery('<li/>');
$elem.attr('data-tag', tagName.toLowerCase());
function getListElement(tagName) {
return $tagList.find('li[data-tag="' + tagName.toLowerCase() + '"]');
}
var $tagLink = jQuery('<a class="tag">');
$tagLink.text(tagName);
$tagLink.click(function(e) {
e.preventDefault();
showOrHideTagSiblings(tagName);
});
$elem.append($tagLink);
function setTags(newTagNames) {
tags = newTagNames.slice();
$tagList.empty();
$underlyingInput.val(newTagNames.join(' '));
_.each(newTagNames, function(tagName) {
var $elem = createListElement(tagName);
$tagList.append($elem);
});
}
var $deleteButton = jQuery('<a class="close"><i class="fa fa-remove"></i></a>');
$deleteButton.click(function(e) {
e.preventDefault();
removeTag(tagName);
$input.focus();
});
$elem.append($deleteButton);
return $elem;
}
function createListElement(tagName) {
var $elem = jQuery('<li/>');
$elem.attr('data-tag', tagName.toLowerCase());
function showOrHideSuggestions(suggestedTagNames) {
if (_.size(suggestedTagNames) === 0) {
return;
}
var $tagLink = jQuery('<a class="tag">');
$tagLink.text(tagName + ' ' /* for easy copying */);
$tagLink.click(function(e) {
e.preventDefault();
showOrHideSiblings(tagName);
showOrHideSuggestions(tagName);
});
$elem.append($tagLink);
var suggestions = filterSuggestions(suggestedTagNames);
if (suggestions.length > 0) {
attachTagsToSuggestionList($suggestions.find('ul'), suggestions);
$suggestions.slideDown('fast');
}
}
var $deleteButton = jQuery('<a class="close"><i class="fa fa-remove"></i></a>');
$deleteButton.click(function(e) {
e.preventDefault();
removeTag(tagName);
$input.focus();
});
$elem.append($deleteButton);
return $elem;
}
function showOrHideTagSiblings(tagName) {
if ($siblings.data('lastTag') === tagName && $siblings.is(':visible')) {
$siblings.slideUp('fast');
$siblings.data('lastTag', null);
return;
}
function showOrHideSuggestions(tagName) {
var tag = getExportedTag(tagName);
var suggestions = tag ? tag.suggestions : [];
updateSuggestions($suggestions, suggestions);
}
promise.wait(getSiblings(tagName), promise.make(function(resolve, reject) {
$siblings.slideUp('fast', resolve);
})).then(function(siblings) {
$siblings.data('lastTag', tagName);
function showOrHideSiblings(tagName) {
if ($siblings.data('lastTag') === tagName && $siblings.is(':visible')) {
$siblings.slideUp('fast');
$siblings.data('lastTag', null);
return;
}
if (!_.size(siblings)) {
return;
}
promise.wait(getSiblings(tagName), promise.make(function(resolve, reject) {
$siblings.slideUp('fast', resolve);
})).then(function(siblings) {
siblings = _.pluck(siblings, 'name');
$siblings.data('lastTag', tagName);
$siblings.data('siblings', siblings);
updateSuggestions($siblings, siblings);
}).fail(function() {
});
}
var suggestions = filterSuggestions(_.pluck(siblings, 'name'));
if (suggestions.length > 0) {
attachTagsToSuggestionList($siblings.find('ul'), suggestions);
$siblings.slideDown('fast');
}
}).fail(function() {
});
}
function refreshShownSiblings() {
updateSuggestions($siblings, $siblings.data('siblings'));
}
function filterSuggestions(sourceTagNames) {
var tagNames = _.filter(sourceTagNames.slice(), function(tagName) {
return !isTaggedWith(tagName);
});
tagNames = tagNames.slice(0, 20);
return tagNames;
}
function updateSuggestions($target, suggestedTagNames) {
function filterSuggestions(sourceTagNames) {
if (!sourceTagNames) {
return [];
}
var tagNames = _.filter(sourceTagNames.slice(), function(tagName) {
return !isTaggedWith(tagName);
});
tagNames = tagNames.slice(0, 20);
return tagNames;
}
function attachTagsToSuggestionList($list, tagNames) {
$list.empty();
_.each(tagNames, function(tagName) {
var $li = jQuery('<li>');
var $a = jQuery('<a href="#/posts/query=' + tagName + '">');
$a.text(tagName);
$a.click(function(e) {
e.preventDefault();
addTag(tagName);
$li.fadeOut('fast', function() {
$li.remove();
if ($list.children().length === 0) {
$list.parent('div').slideUp('fast');
}
});
});
$li.append($a);
$list.append($li);
});
}
function attachTagsToSuggestionList($list, tagNames) {
$list.empty();
_.each(tagNames, function(tagName) {
var $li = jQuery('<li>');
var $a = jQuery('<a href="#/posts/query=' + tagName + '">');
$a.text(tagName);
$a.click(function(e) {
e.preventDefault();
addTag(tagName, SOURCE_SUGGESTIONS);
$li.fadeOut('fast', function() {
$li.remove();
if ($list.children().length === 0) {
$list.parent('div').slideUp('fast');
}
});
});
$li.append($a);
$list.append($li);
});
}
function getSiblings(tagName) {
return promise.make(function(resolve, reject) {
promise.wait(api.get('/tags/' + tagName + '/siblings'))
.then(function(response) {
resolve(response.json.data);
}).fail(function() {
reject();
});
});
}
var suggestions = filterSuggestions(suggestedTagNames);
if (suggestions.length > 0) {
attachTagsToSuggestionList($target.find('ul'), suggestions);
$target.slideDown('fast');
} else {
$target.slideUp('fast');
}
}
function getTags() {
return tags;
}
function getSiblings(tagName) {
return promise.make(function(resolve, reject) {
promise.wait(api.get('/tags/' + tagName + '/siblings'))
.then(function(response) {
resolve(response.json.tags);
}).fail(function() {
reject();
});
});
}
function focus() {
$input.focus();
}
function getTags() {
return tags;
}
function hideSuggestions() {
$siblings.hide();
$suggestions.hide();
}
function focus() {
$input.focus();
}
_.extend(options, {
setTags: setTags,
getTags: getTags,
removeTag: removeTag,
addTag: addTag,
focus: focus,
hideSuggestions: hideSuggestions,
});
return options;
function hideSuggestions() {
$siblings.hide();
$suggestions.hide();
$siblings.data('siblings', []);
}
_.extend(options, {
setTags: setTags,
getTags: getTags,
removeTag: removeTag,
addTag: addTag,
focus: focus,
hideSuggestions: hideSuggestions,
});
return options;
};

View File

@ -2,53 +2,53 @@ var App = App || {};
App.DI = (function() {
var factories = {};
var instances = {};
var factories = {};
var instances = {};
function get(key) {
var instance = instances[key];
if (!instance) {
var factory = factories[key];
if (!factory) {
throw new Error('Unregistered key: ' + key);
}
var objectInitializer = factory.initializer;
var singleton = factory.singleton;
var deps = resolveDependencies(objectInitializer, factory.dependencies);
instance = {};
instance = objectInitializer.apply(instance, deps);
if (singleton) {
instances[key] = instance;
}
}
return instance;
}
function get(key) {
var instance = instances[key];
if (!instance) {
var factory = factories[key];
if (!factory) {
throw new Error('Unregistered key: ' + key);
}
var objectInitializer = factory.initializer;
var singleton = factory.singleton;
var deps = resolveDependencies(objectInitializer, factory.dependencies);
instance = {};
instance = objectInitializer.apply(instance, deps);
if (singleton) {
instances[key] = instance;
}
}
return instance;
}
function resolveDependencies(objectIntializer, depKeys) {
var deps = [];
for (var i = 0; i < depKeys.length; i ++) {
deps[i] = get(depKeys[i]);
}
return deps;
}
function resolveDependencies(objectIntializer, depKeys) {
var deps = [];
for (var i = 0; i < depKeys.length; i ++) {
deps[i] = get(depKeys[i]);
}
return deps;
}
function register(key, dependencies, objectInitializer) {
factories[key] = {initializer: objectInitializer, singleton: false, dependencies: dependencies};
}
function register(key, dependencies, objectInitializer) {
factories[key] = {initializer: objectInitializer, singleton: false, dependencies: dependencies};
}
function registerSingleton(key, dependencies, objectInitializer) {
factories[key] = {initializer: objectInitializer, singleton: true, dependencies: dependencies};
}
function registerSingleton(key, dependencies, objectInitializer) {
factories[key] = {initializer: objectInitializer, singleton: true, dependencies: dependencies};
}
function registerManual(key, objectInitializer) {
instances[key] = objectInitializer();
}
function registerManual(key, objectInitializer) {
instances[key] = objectInitializer();
}
return {
get: get,
register: register,
registerManual: registerManual,
registerSingleton: registerSingleton,
};
return {
get: get,
register: register,
registerManual: registerManual,
registerSingleton: registerSingleton,
};
})();

View File

@ -1,50 +1,61 @@
var App = App || {};
App.Keyboard = function(jQuery, mousetrap) {
App.Keyboard = function(jQuery, mousetrap, browsingSettings) {
var oldStopCallback = mousetrap.stopCallback;
mousetrap.stopCallback = function(e, element, combo, sequence) {
if (combo.indexOf('ctrl') === -1 && e.ctrlKey) {
return true;
}
if (combo.indexOf('alt') === -1 && e.altKey) {
return true;
}
if (combo.indexOf('ctrl') !== -1) {
return false;
}
var $focused = jQuery(':focus').eq(0);
if ($focused.length && $focused.prop('tagName').match(/embed|object/i)) {
return true;
}
return oldStopCallback.apply(mousetrap, arguments);
};
var enabled = browsingSettings.getSettings().keyboardShortcuts;
var oldStopCallback = mousetrap.stopCallback;
mousetrap.stopCallback = function(e, element, combo, sequence) {
if (combo.indexOf('ctrl') === -1 && e.ctrlKey) {
return true;
}
if (combo.indexOf('alt') === -1 && e.altKey) {
return true;
}
if (combo.indexOf('ctrl') !== -1) {
return false;
}
var $focused = jQuery(':focus').eq(0);
if ($focused.length) {
if ($focused.prop('tagName').match(/embed|object/i)) {
return true;
}
if ($focused.prop('tagName').toLowerCase() === 'input' &&
$focused.attr('type').match(/checkbox|radio/i)) {
return false;
}
}
return oldStopCallback.apply(mousetrap, arguments);
};
function keyup(key, callback) {
unbind(key);
mousetrap.bind(key, callback, 'keyup');
}
function keyup(key, callback) {
unbind(key);
if (enabled) {
mousetrap.bind(key, callback, 'keyup');
}
}
function keydown(key, callback) {
unbind(key);
mousetrap.bind(key, callback);
}
function keydown(key, callback) {
unbind(key);
if (enabled) {
mousetrap.bind(key, callback);
}
}
function reset() {
mousetrap.reset();
}
function reset() {
mousetrap.reset();
}
function unbind(key) {
mousetrap.unbind(key, 'keyup');
mousetrap.unbind(key);
}
function unbind(key) {
mousetrap.unbind(key, 'keyup');
mousetrap.unbind(key);
}
return {
keydown: keydown,
keyup: keyup,
reset: reset,
unbind: unbind,
};
return {
keydown: keydown,
keyup: keyup,
reset: reset,
unbind: unbind,
};
};
App.DI.register('keyboard', ['jQuery', 'mousetrap'], App.Keyboard);
App.DI.register('keyboard', ['jQuery', 'mousetrap', 'browsingSettings'], App.Keyboard);

View File

@ -1,142 +1,139 @@
var App = App || {};
App.Pager = function(
_,
promise,
api) {
_,
promise,
api) {
var totalPages;
var pageNumber;
var searchParams;
var url;
var cache = {};
var totalPages;
var pageNumber;
var searchParams;
var url;
var cache = {};
function init(args) {
url = args.url;
function init(args) {
url = args.url;
setSearchParams(args.searchParams);
if (typeof(args.page) !== 'undefined') {
setPage(args.page);
} else {
setPage(1);
}
}
setSearchParams(args.searchParams);
if (typeof(args.page) !== 'undefined') {
setPage(args.page);
} else {
setPage(1);
}
}
function getPage() {
return pageNumber;
}
function getPage() {
return pageNumber;
}
function getTotalPages() {
return totalPages;
}
function getTotalPages() {
return totalPages;
}
function prevPage() {
if (pageNumber > 1) {
setPage(pageNumber - 1);
return true;
}
return false;
}
function prevPage() {
if (pageNumber > 1) {
setPage(pageNumber - 1);
return true;
}
return false;
}
function nextPage() {
if (pageNumber < totalPages) {
setPage(pageNumber + 1);
return true;
}
return false;
}
function nextPage() {
if (pageNumber < totalPages) {
setPage(pageNumber + 1);
return true;
}
return false;
}
function setPage(newPageNumber) {
pageNumber = parseInt(newPageNumber);
if (!pageNumber || isNaN(pageNumber)) {
throw new Error('Trying to set page to a non-number (' + newPageNumber + ')');
}
}
function setPage(newPageNumber) {
pageNumber = parseInt(newPageNumber);
if (!pageNumber || isNaN(pageNumber)) {
throw new Error('Trying to set page to a non-number (' + newPageNumber + ')');
}
}
function getSearchParams() {
return searchParams;
}
function getSearchParams() {
return searchParams;
}
function setSearchParams(newSearchParams) {
setPage(1);
searchParams = _.extend({}, newSearchParams);
delete searchParams.page;
}
function setSearchParams(newSearchParams) {
setPage(1);
searchParams = _.extend({}, newSearchParams);
delete searchParams.page;
}
function retrieve() {
return promise.make(function(resolve, reject) {
promise.wait(api.get(url, _.extend({}, searchParams, {page: pageNumber})))
.then(function(response) {
var pageSize = response.json.pageSize;
var totalRecords = response.json.totalRecords;
totalPages = Math.ceil(totalRecords / pageSize);
function retrieve() {
return promise.make(function(resolve, reject) {
promise.wait(api.get(url, _.extend({}, searchParams, {page: pageNumber})))
.then(function(response) {
var pageSize = response.json.pageSize;
var totalRecords = response.json.totalRecords;
totalPages = Math.ceil(totalRecords / pageSize);
resolve({
entities: response.json.data,
totalRecords: totalRecords,
totalPages: totalPages});
resolve(response);
}).fail(function(response) {
reject(response);
});
});
}
}).fail(function(response) {
reject(response);
});
});
}
function retrieveCached() {
return promise.make(function(resolve, reject) {
var cacheKey = JSON.stringify(_.extend({}, searchParams, {url: url, page: getPage()}));
if (cacheKey in cache) {
resolve.apply(this, cache[cacheKey]);
} else {
promise.wait(retrieve())
.then(function() {
cache[cacheKey] = arguments;
resolve.apply(this, arguments);
}).fail(function() {
reject.apply(this, arguments);
});
}
});
}
function retrieveCached() {
return promise.make(function(resolve, reject) {
var cacheKey = JSON.stringify(_.extend({}, searchParams, {url: url, page: getPage()}));
if (cacheKey in cache) {
resolve.apply(this, cache[cacheKey]);
} else {
promise.wait(retrieve())
.then(function() {
cache[cacheKey] = arguments;
resolve.apply(this, arguments);
}).fail(function() {
reject.apply(this, arguments);
});
}
});
}
function resetCache() {
cache = {};
}
function resetCache() {
cache = {};
}
function getVisiblePages() {
var pages = [1, totalPages || 1];
var pagesAroundCurrent = 2;
for (var i = -pagesAroundCurrent; i <= pagesAroundCurrent; i ++) {
if (pageNumber + i >= 1 && pageNumber + i <= totalPages) {
pages.push(pageNumber + i);
}
}
if (pageNumber - pagesAroundCurrent - 1 === 2) {
pages.push(2);
}
if (pageNumber + pagesAroundCurrent + 1 === totalPages - 1) {
pages.push(totalPages - 1);
}
function getVisiblePages() {
var pages = [1, totalPages || 1];
var pagesAroundCurrent = 2;
for (var i = -pagesAroundCurrent; i <= pagesAroundCurrent; i ++) {
if (pageNumber + i >= 1 && pageNumber + i <= totalPages) {
pages.push(pageNumber + i);
}
}
if (pageNumber - pagesAroundCurrent - 1 === 2) {
pages.push(2);
}
if (pageNumber + pagesAroundCurrent + 1 === totalPages - 1) {
pages.push(totalPages - 1);
}
return pages.sort(function(a, b) { return a - b; }).filter(function(item, pos) {
return !pos || item !== pages[pos - 1];
});
}
return pages.sort(function(a, b) { return a - b; }).filter(function(item, pos) {
return !pos || item !== pages[pos - 1];
});
}
return {
init: init,
getPage: getPage,
getTotalPages: getTotalPages,
prevPage: prevPage,
nextPage: nextPage,
setPage: setPage,
getSearchParams: getSearchParams,
setSearchParams: setSearchParams,
retrieve: retrieve,
retrieveCached: retrieveCached,
getVisiblePages: getVisiblePages,
resetCache: resetCache,
};
return {
init: init,
getPage: getPage,
getTotalPages: getTotalPages,
prevPage: prevPage,
nextPage: nextPage,
setPage: setPage,
getSearchParams: getSearchParams,
setSearchParams: setSearchParams,
retrieve: retrieve,
retrieveCached: retrieveCached,
getVisiblePages: getVisiblePages,
resetCache: resetCache,
};
};

View File

@ -2,53 +2,53 @@ var App = App || {};
App.PresenterManager = function(jQuery, promise, topNavigationPresenter, keyboard) {
var lastContentPresenter = null;
var lastContentPresenter = null;
function init() {
return promise.make(function(resolve, reject) {
initPresenter(topNavigationPresenter, [], resolve);
});
}
function init() {
return promise.make(function(resolve, reject) {
initPresenter(topNavigationPresenter, [], resolve);
});
}
function initPresenter(presenter, args, loaded) {
presenter.init.call(presenter, args, loaded);
}
function initPresenter(presenter, args, loaded) {
presenter.init.call(presenter, args, loaded);
}
function switchContentPresenter(presenter, args) {
if (lastContentPresenter === null || lastContentPresenter.name !== presenter.name) {
if (lastContentPresenter !== null && lastContentPresenter.deinit) {
lastContentPresenter.deinit();
}
keyboard.reset();
topNavigationPresenter.changeTitle(null);
topNavigationPresenter.focus();
presenter.init.call(presenter, args, function() {});
lastContentPresenter = presenter;
} else if (lastContentPresenter.reinit) {
lastContentPresenter.reinit.call(lastContentPresenter, args, function() {});
}
}
function switchContentPresenter(presenter, args) {
if (lastContentPresenter === null || lastContentPresenter.name !== presenter.name) {
if (lastContentPresenter !== null && lastContentPresenter.deinit) {
lastContentPresenter.deinit();
}
keyboard.reset();
topNavigationPresenter.changeTitle(null);
topNavigationPresenter.focus();
presenter.init.call(presenter, args, function() {});
lastContentPresenter = presenter;
} else if (lastContentPresenter.reinit) {
lastContentPresenter.reinit.call(lastContentPresenter, args, function() {});
}
}
function initPresenters(options, loaded) {
var count = 0;
var subPresenterLoaded = function() {
count ++;
if (count === options.length) {
loaded();
}
};
function initPresenters(options, loaded) {
var count = 0;
var subPresenterLoaded = function() {
count ++;
if (count === options.length) {
loaded();
}
};
for (var i = 0; i < options.length; i ++) {
initPresenter(options[i][0], options[i][1], subPresenterLoaded);
}
}
for (var i = 0; i < options.length; i ++) {
initPresenter(options[i][0], options[i][1], subPresenterLoaded);
}
}
return {
init: init,
initPresenter: initPresenter,
initPresenters: initPresenters,
switchContentPresenter: switchContentPresenter,
};
return {
init: init,
initPresenter: initPresenter,
initPresenters: initPresenters,
switchContentPresenter: switchContentPresenter,
};
};

View File

@ -0,0 +1,232 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.CommentListPresenter = function(
_,
jQuery,
util,
promise,
api,
auth,
topNavigationPresenter,
messagePresenter) {
var $el;
var privileges;
var templates = {};
var post;
var comments = [];
function init(params, loaded) {
$el = params.$target;
post = params.post;
comments = params.comments || [];
privileges = {
canListComments: auth.hasPrivilege(auth.privileges.listComments),
canAddComments: auth.hasPrivilege(auth.privileges.addComments),
canEditOwnComments: auth.hasPrivilege(auth.privileges.editOwnComments),
canEditAllComments: auth.hasPrivilege(auth.privileges.editAllComments),
canDeleteOwnComments: auth.hasPrivilege(auth.privileges.deleteOwnComments),
canDeleteAllComments: auth.hasPrivilege(auth.privileges.deleteAllComments),
canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers),
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
};
promise.wait(
util.promiseTemplate('comment-list'),
util.promiseTemplate('comment-list-item'),
util.promiseTemplate('comment-form'))
.then(function(
commentListTemplate,
commentListItemTemplate,
commentFormTemplate)
{
templates.commentList = commentListTemplate;
templates.commentListItem = commentListItemTemplate;
templates.commentForm = commentFormTemplate;
render();
loaded();
if (comments.length === 0) {
promise.wait(api.get('/comments/' + params.post.id))
.then(function(response) {
comments = response.json.comments;
render();
}).fail(function() {
console.log(arguments);
});
}
})
.fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$el.html(templates.commentList(
_.extend(
{
commentListItemTemplate: templates.commentListItem,
commentFormTemplate: templates.commentForm,
util: util,
comments: comments,
post: post,
},
privileges)));
$el.find('.comment-add form button[type=submit]').click(function(e) { commentFormSubmitted(e, null); });
renderComments(comments);
}
function renderComments(comments) {
var $target = $el.find('.comments');
var $targetList = $el.find('ul');
if (comments.length > 0) {
$target.show();
} else {
$target.hide();
}
$targetList.empty();
_.each(comments, function(comment) {
renderComment($targetList, comment);
});
}
function renderComment($targetList, comment) {
var $item = jQuery('<li>' + templates.commentListItem({
comment: comment,
util: util,
canVote: auth.isLoggedIn(),
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.canEditOwnComments : privileges.canEditAllComments,
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.canDeleteOwnComments : privileges.canDeleteAllComments,
canViewUsers: privileges.canViewUsers,
canViewPosts: privileges.canViewPosts,
}) + '</li>');
util.loadImagesNicely($item.find('img'));
$targetList.append($item);
$item.find('a.edit').click(function(e) {
e.preventDefault();
editCommentStart($item, comment);
});
$item.find('a.delete').click(function(e) {
e.preventDefault();
deleteComment(comment);
});
$item.find('a.score-up').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : 1);
});
$item.find('a.score-down').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : -1);
});
}
function commentFormSubmitted(e, comment) {
e.preventDefault();
var $button = jQuery(e.target);
var $form = $button.parents('form');
var sender = $button.val();
if (sender === 'preview') {
previewComment($form);
} else {
submitComment($form, comment);
}
}
function previewComment($form) {
var $preview = $form.find('.preview');
$preview.slideUp('fast', function() {
$preview.html(util.formatMarkdown($form.find('textarea').val()));
$preview.slideDown('fast');
});
}
function updateComment(comment) {
comments = _.map(comments, function(c) { return c.id === comment.id ? comment : c; });
render();
}
function addComment(comment) {
comments.push(comment);
render();
}
function submitComment($form, commentToEdit) {
$form.find('.preview').slideUp();
var $textarea = $form.find('textarea');
var data = {text: $textarea.val()};
var p;
if (commentToEdit) {
p = promise.wait(api.put('/comments/' + commentToEdit.id, data));
} else {
p = promise.wait(api.post('/comments/' + post.id, data));
}
p.then(function(response) {
$textarea.val('');
var comment = response.json.comment;
if (commentToEdit) {
$form.slideUp(function() {
$form.remove();
});
updateComment(comment);
} else {
addComment(comment);
}
}).fail(showGenericError);
}
function editCommentStart($item, comment) {
if ($item.find('.comment-form').length > 0) {
return;
}
var $form = jQuery(templates.commentForm({title: 'Edit comment', text: comment.text}));
$item.find('.body').append($form);
$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
}
function deleteComment(comment) {
if (!window.confirm('Are you sure you want to delete this comment?')) {
return;
}
promise.wait(api.delete('/comments/' + comment.id))
.then(function(response) {
comments = _.filter(comments, function(c) { return c.id !== comment.id; });
renderComments(comments);
}).fail(showGenericError);
}
function score(comment, scoreValue) {
promise.wait(api.post('/comments/' + comment.id + '/score', {score: scoreValue}))
.then(function(response) {
comment.score = parseInt(response.json.score);
comment.ownScore = parseInt(response.json.ownScore);
updateComment(comment);
}).fail(showGenericError);
}
function showGenericError(response) {
window.alert(response.json && response.json.error || response);
}
return {
init: init,
render: render,
};
};
App.DI.register('commentListPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.CommentListPresenter);

View File

@ -2,97 +2,103 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.GlobalCommentListPresenter = function(
_,
jQuery,
util,
promise,
pagerPresenter,
topNavigationPresenter) {
_,
jQuery,
util,
auth,
promise,
pagerPresenter,
topNavigationPresenter) {
var $el;
var templates = {};
var $el;
var privileges;
var templates = {};
function init(params, loaded) {
$el = jQuery('#content');
topNavigationPresenter.select('comments');
function init(params, loaded) {
$el = jQuery('#content');
topNavigationPresenter.select('comments');
promise.wait(
util.promiseTemplate('global-comment-list'),
util.promiseTemplate('global-comment-list-item'),
util.promiseTemplate('post-list-item'))
.then(function(listTemplate, listItemTemplate, postTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
templates.post = postTemplate;
privileges = {
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
};
render();
loaded();
promise.wait(
util.promiseTemplate('global-comment-list'),
util.promiseTemplate('global-comment-list-item'),
util.promiseTemplate('post-list-item'))
.then(function(listTemplate, listItemTemplate, postTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
templates.post = postTemplate;
pagerPresenter.init({
baseUri: '#/comments',
backendUri: '/comments',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderComments($page, data.entities);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
render();
loaded();
pagerPresenter.init({
baseUri: '#/comments',
backendUri: '/comments',
$target: $el.find('.pagination-target'),
updateCallback: function($page, response) {
renderComments($page, response.json.comments);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(params, loaded) {
pagerPresenter.reinit({query: params.query});
loaded();
}
function reinit(params, loaded) {
pagerPresenter.reinit({query: params.query || {}});
loaded();
}
function deinit() {
pagerPresenter.deinit();
}
function deinit() {
pagerPresenter.deinit();
}
function render() {
$el.html(templates.list());
}
function render() {
$el.html(templates.list());
}
function renderComments($page, data) {
var $target = $page.find('.posts');
_.each(data, function(data) {
var post = data.post;
var comments = data.comments;
function renderComments($page, postComments) {
var $target = $page.find('.posts');
_.each(postComments, function(postComments) {
var post = postComments.post;
var comments = postComments.comments;
var $post = jQuery('<li>' + templates.listItem({
util: util,
post: post,
postTemplate: templates.post,
canViewPosts: privileges.canViewPosts,
}) + '</li>');
var $post = jQuery('<li>' + templates.listItem({
util: util,
post: post,
postTemplate: templates.post,
}) + '</li>');
util.loadImagesNicely($post.find('img'));
var presenter = App.DI.get('commentListPresenter');
util.loadImagesNicely($post.find('img'));
var presenter = App.DI.get('postCommentListPresenter');
presenter.init({
post: post,
comments: comments,
$target: $post.find('.post-comments-target'),
}, function() {
presenter.render();
});
presenter.init({
post: post,
comments: comments,
$target: $post.find('.post-comments-target'),
}, function() {
presenter.render();
});
$target.append($post);
});
}
$target.append($post);
});
}
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
};
App.DI.register('globalCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.GlobalCommentListPresenter);
App.DI.register('globalCommentListPresenter', ['_', 'jQuery', 'util', 'auth', 'promise', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.GlobalCommentListPresenter);

View File

@ -2,48 +2,48 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.HelpPresenter = function(
jQuery,
promise,
util,
topNavigationPresenter) {
jQuery,
promise,
util,
topNavigationPresenter) {
var $el = jQuery('#content');
var templates = {};
var activeTab;
var $el = jQuery('#content');
var templates = {};
var activeTab;
function init(params, loaded) {
topNavigationPresenter.select('help');
topNavigationPresenter.changeTitle('Help');
function init(params, loaded) {
topNavigationPresenter.select('help');
topNavigationPresenter.changeTitle('Help');
promise.wait(util.promiseTemplate('help'))
.then(function(template) {
templates.help = template;
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('help'))
.then(function(template) {
templates.help = template;
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(params, loaded) {
activeTab = params.tab || 'about';
render();
loaded();
}
function reinit(params, loaded) {
activeTab = params.tab || 'about';
render();
loaded();
}
function render() {
$el.html(templates.help({title: topNavigationPresenter.getBaseTitle() }));
$el.find('.big-button').removeClass('active');
$el.find('.big-button[href*="' + activeTab + '"]').addClass('active');
$el.find('div[data-tab]').hide();
$el.find('div[data-tab*="' + activeTab + '"]').show();
}
function render() {
$el.html(templates.help({title: topNavigationPresenter.getBaseTitle() }));
$el.find('.big-button').removeClass('active');
$el.find('.big-button[href*="' + activeTab + '"]').addClass('active');
$el.find('div[data-tab]').hide();
$el.find('div[data-tab*="' + activeTab + '"]').show();
}
return {
init: init,
reinit: reinit,
render: render,
};
return {
init: init,
reinit: reinit,
render: render,
};
};

View File

@ -2,77 +2,76 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.HistoryPresenter = function(
_,
jQuery,
util,
promise,
auth,
pagerPresenter,
topNavigationPresenter) {
_,
jQuery,
util,
promise,
auth,
pagerPresenter,
topNavigationPresenter) {
var $el = jQuery('#content');
var templates = {};
var params;
var $el = jQuery('#content');
var templates = {};
var params;
function init(params, loaded) {
topNavigationPresenter.changeTitle('History');
function init(params, loaded) {
topNavigationPresenter.changeTitle('History');
promise.wait(
util.promiseTemplate('global-history'),
util.promiseTemplate('history'))
.then(function(historyWrapperTemplate, historyTemplate) {
templates.historyWrapper = historyWrapperTemplate;
templates.history = historyTemplate;
promise.wait(
util.promiseTemplate('global-history'),
util.promiseTemplate('history'))
.then(function(historyWrapperTemplate, historyTemplate) {
templates.historyWrapper = historyWrapperTemplate;
templates.history = historyTemplate;
render();
loaded();
render();
loaded();
pagerPresenter.init({
baseUri: '#/history',
backendUri: '/history',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderHistory($page, data.entities);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
pagerPresenter.init({
baseUri: '#/history',
backendUri: '/history',
$target: $el.find('.pagination-target'),
updateCallback: function($page, response) {
renderHistory($page, response.json.history);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
pagerPresenter.reinit({query: params.query});
loaded();
}
pagerPresenter.reinit({query: params.query});
loaded();
}
function deinit() {
pagerPresenter.deinit();
}
function deinit() {
pagerPresenter.deinit();
}
function render() {
$el.html(templates.historyWrapper());
}
function render() {
$el.html(templates.historyWrapper());
}
function renderHistory($page, historyItems) {
$page.append(templates.history({
formatRelativeTime: util.formatRelativeTime,
history: historyItems}));
}
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
function renderHistory($page, historyItems) {
$page.append(templates.history({
util: util,
history: historyItems}));
}
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
};
App.DI.register('historyPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.HistoryPresenter);

View File

@ -2,84 +2,90 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.HomePresenter = function(
jQuery,
util,
promise,
api,
auth,
presenterManager,
postContentPresenter,
topNavigationPresenter,
messagePresenter) {
jQuery,
util,
promise,
api,
auth,
presenterManager,
postContentPresenter,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content');
var templates = {};
var globals;
var post;
var user;
var $el = jQuery('#content');
var templates = {};
var globals;
var post;
var user;
function init(params, loaded) {
topNavigationPresenter.select('home');
topNavigationPresenter.changeTitle('Home');
function init(params, loaded) {
topNavigationPresenter.select('home');
topNavigationPresenter.changeTitle('Home');
promise.wait(
util.promiseTemplate('home'),
api.get('/globals'),
api.get('/posts/featured'))
.then(function(
homeTemplate,
globalsResponse,
featuredPostResponse) {
templates.home = homeTemplate;
promise.wait(
util.promiseTemplate('home'),
api.get('/globals'),
api.get('/posts/featured'))
.then(function(
homeTemplate,
globalsResponse,
featuredPostResponse) {
templates.home = homeTemplate;
globals = globalsResponse.json;
post = featuredPostResponse.json.post;
user = featuredPostResponse.json.user;
render();
loaded();
globals = globalsResponse.json;
post = featuredPostResponse.json.post;
user = featuredPostResponse.json.user;
render();
loaded();
if ($el.find('#post-content-target').length > 0) {
presenterManager.initPresenters([
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}]],
function() {});
}
if ($el.find('#post-content-target').length > 0) {
presenterManager.initPresenters([
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}]],
function() {
var $wrapper = $el.find('.object-wrapper');
$wrapper.css({
maxWidth: $wrapper.attr('data-width') + 'px',
width: 'auto',
margin: '0 auto'});
postContentPresenter.updatePostNotesSize();
});
}
}).fail(function(response) {
messagePresenter.showError($el, response.json && response.json.error || response);
loaded();
});
}
}).fail(function(response) {
messagePresenter.showError($el, response.json && response.json.error || response);
loaded();
});
}
function render() {
$el.html(templates.home({
post: post,
user: user,
globals: globals,
title: topNavigationPresenter.getBaseTitle(),
canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers),
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
formatRelativeTime: util.formatRelativeTime,
formatFileSize: util.formatFileSize,
version: jQuery('head').attr('data-version'),
buildTime: jQuery('head').attr('data-build-time'),
}));
}
function render() {
$el.html(templates.home({
post: post,
user: user,
globals: globals,
title: topNavigationPresenter.getBaseTitle(),
canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers),
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
util: util,
version: jQuery('head').attr('data-version'),
buildTime: jQuery('head').attr('data-build-time'),
}));
}
return {
init: init,
render: render,
};
return {
init: init,
render: render,
};
};
App.DI.register('homePresenter', [
'jQuery',
'util',
'promise',
'api',
'auth',
'presenterManager',
'postContentPresenter',
'topNavigationPresenter',
'messagePresenter'],
App.Presenters.HomePresenter);
'jQuery',
'util',
'promise',
'api',
'auth',
'presenterManager',
'postContentPresenter',
'topNavigationPresenter',
'messagePresenter'],
App.Presenters.HomePresenter);

View File

@ -2,46 +2,46 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.HttpErrorPresenter = function(
jQuery,
promise,
util,
topNavigationPresenter) {
jQuery,
promise,
util,
topNavigationPresenter) {
var $el = jQuery('#content');
var templates = {};
var $el = jQuery('#content');
var templates = {};
function init(params, loaded) {
topNavigationPresenter.changeTitle('Error ' + params.error);
function init(params, loaded) {
topNavigationPresenter.changeTitle('Error ' + params.error);
if (params.error === 404) {
promise.wait(util.promiseTemplate('404'))
.then(function(template) {
templates.errorPage = template;
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
} else {
console.log('Not supported.');
loaded();
}
}
if (params.error === 404) {
promise.wait(util.promiseTemplate('404'))
.then(function(template) {
templates.errorPage = template;
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
} else {
console.log('Not supported.');
loaded();
}
}
function reinit(params, loaded) {
render();
loaded();
}
function reinit(params, loaded) {
render();
loaded();
}
function render() {
$el.html(templates.errorPage());
}
function render() {
$el.html(templates.errorPage());
}
return {
init: init,
reinit: reinit,
render: render,
};
return {
init: init,
reinit: reinit,
render: render,
};
};

View File

@ -2,84 +2,84 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.LoginPresenter = function(
jQuery,
util,
promise,
router,
auth,
topNavigationPresenter,
messagePresenter) {
jQuery,
util,
promise,
router,
auth,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content');
var $messages;
var templates = {};
var previousLocation;
var $el = jQuery('#content');
var $messages;
var templates = {};
var previousLocation;
function init(params, loaded) {
topNavigationPresenter.select('login');
topNavigationPresenter.changeTitle('Login');
previousLocation = params.previousLocation;
promise.wait(util.promiseTemplate('login-form'))
.then(function(template) {
templates.login = template;
if (auth.isLoggedIn()) {
finishLogin();
} else {
render();
$el.find('input:eq(0)').focus();
}
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function init(params, loaded) {
topNavigationPresenter.select('login');
topNavigationPresenter.changeTitle('Login');
previousLocation = params.previousLocation;
promise.wait(util.promiseTemplate('login-form'))
.then(function(template) {
templates.login = template;
if (auth.isLoggedIn()) {
finishLogin();
} else {
render();
$el.find('input:eq(0)').focus();
}
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$el.html(templates.login());
$el.find('form').submit(loginFormSubmitted);
$messages = $el.find('.messages');
$messages.width($el.find('form').width());
}
function render() {
$el.html(templates.login());
$el.find('form').submit(loginFormSubmitted);
$messages = $el.find('.messages');
$messages.width($el.find('form').width());
}
function loginFormSubmitted(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
function loginFormSubmitted(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
var userNameOrEmail = $el.find('[name=user]').val();
var password = $el.find('[name=password]').val();
var remember = $el.find('[name=remember]').is(':checked');
var userNameOrEmail = $el.find('[name=user]').val();
var password = $el.find('[name=password]').val();
var remember = $el.find('[name=remember]').is(':checked');
if (userNameOrEmail.length === 0) {
messagePresenter.showError($messages, 'User name cannot be empty.');
return false;
}
if (userNameOrEmail.length === 0) {
messagePresenter.showError($messages, 'User name cannot be empty.');
return false;
}
if (password.length === 0) {
messagePresenter.showError($messages, 'Password cannot be empty.');
return false;
}
if (password.length === 0) {
messagePresenter.showError($messages, 'Password cannot be empty.');
return false;
}
promise.wait(auth.loginFromCredentials(userNameOrEmail, password, remember))
.then(function(response) {
finishLogin();
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
promise.wait(auth.loginFromCredentials(userNameOrEmail, password, remember))
.then(function(response) {
finishLogin();
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
function finishLogin() {
if (previousLocation && !previousLocation.match(/logout|password-reset|activate|register/)) {
router.navigate(previousLocation);
} else {
router.navigateToMainPage();
}
}
function finishLogin() {
if (previousLocation && !previousLocation.match(/logout|password-reset|activate|register/)) {
router.navigate(previousLocation);
} else {
router.navigateToMainPage();
}
}
return {
init: init,
render: render,
};
return {
init: init,
render: render,
};
};

View File

@ -2,38 +2,38 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.LogoutPresenter = function(
jQuery,
promise,
router,
auth,
topNavigationPresenter,
messagePresenter) {
jQuery,
promise,
router,
auth,
topNavigationPresenter,
messagePresenter) {
var $messages = jQuery('#content');
var $messages = jQuery('#content');
function init(params, loaded) {
topNavigationPresenter.select('logout');
topNavigationPresenter.changeTitle('Logout');
promise.wait(auth.logout())
.then(function() {
loaded();
$messages.empty();
var $messageDiv = messagePresenter.showInfo($messages, 'Logged out. <a href="">Back to main page</a>');
$messageDiv.find('a').click(mainPageLinkClicked);
}).fail(function(response) {
messagePresenter.showError(($messages, response.json && response.json.error || response) + '<br/>Reload the page to continue.');
loaded();
});
}
function init(params, loaded) {
topNavigationPresenter.select('logout');
topNavigationPresenter.changeTitle('Logout');
promise.wait(auth.logout())
.then(function() {
loaded();
$messages.empty();
var $messageDiv = messagePresenter.showInfo($messages, 'Logged out. <a href="">Back to main page</a>');
$messageDiv.find('a').click(mainPageLinkClicked);
}).fail(function(response) {
messagePresenter.showError(($messages, response.json && response.json.error || response) + '<br/>Reload the page to continue.');
loaded();
});
}
function mainPageLinkClicked(e) {
e.preventDefault();
router.navigateToMainPage();
}
function mainPageLinkClicked(e) {
e.preventDefault();
router.navigateToMainPage();
}
return {
init: init
};
return {
init: init
};
};

View File

@ -3,51 +3,51 @@ App.Presenters = App.Presenters || {};
App.Presenters.MessagePresenter = function(_, jQuery) {
var options = {
instant: false
};
var options = {
instant: false
};
function showInfo($el, message) {
return showMessage($el, 'info', message);
}
function showInfo($el, message) {
return showMessage($el, 'info', message);
}
function showError($el, message) {
return showMessage($el, 'error', message);
}
function showError($el, message) {
return showMessage($el, 'error', message);
}
function hideMessages($el) {
var $messages = $el.children('.message');
if (options.instant) {
$messages.each(function() {
jQuery(this).slideUp('fast', function() {
jQuery(this).remove();
});
});
} else {
$messages.remove();
}
}
function hideMessages($el) {
var $messages = $el.children('.message');
if (options.instant) {
$messages.each(function() {
jQuery(this).slideUp('fast', function() {
jQuery(this).remove();
});
});
} else {
$messages.remove();
}
}
function showMessage($el, className, message) {
var $messageDiv = jQuery('<div>');
$messageDiv.addClass('message');
$messageDiv.addClass(className);
$messageDiv.html(message);
if (!options.instant) {
$messageDiv.hide();
}
$el.append($messageDiv);
if (!options.instant) {
$messageDiv.slideDown('fast');
}
return $messageDiv;
}
function showMessage($el, className, message) {
var $messageDiv = jQuery('<div>');
$messageDiv.addClass('message');
$messageDiv.addClass(className);
$messageDiv.html(message);
if (!options.instant) {
$messageDiv.hide();
}
$el.append($messageDiv);
if (!options.instant) {
$messageDiv.slideDown('fast');
}
return $messageDiv;
}
return _.extend(options, {
showInfo: showInfo,
showError: showError,
hideMessages: hideMessages,
});
return _.extend(options, {
showInfo: showInfo,
showError: showError,
hideMessages: hideMessages,
});
};

View File

@ -2,252 +2,275 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PagerPresenter = function(
_,
jQuery,
util,
promise,
keyboard,
router,
pager,
messagePresenter,
browsingSettings,
progress) {
_,
jQuery,
util,
promise,
keyboard,
router,
pager,
messagePresenter,
browsingSettings,
progress) {
var $target;
var $pageList;
var $messages;
var targetContent;
var endlessScroll = browsingSettings.getSettings().endlessScroll;
var scrollInterval;
var templates = {};
var forceClear = !endlessScroll;
var $target;
var $pageList;
var $messages;
var targetContent;
var endlessScroll = browsingSettings.getSettings().endlessScroll;
var scrollInterval;
var templates = {};
var forceClear = !endlessScroll;
var baseUri;
var updateCallback;
var baseUri;
var updateCallback;
function init(params, loaded) {
baseUri = params.baseUri;
updateCallback = params.updateCallback;
function init(params, loaded) {
baseUri = params.baseUri;
updateCallback = params.updateCallback;
messagePresenter.instant = true;
messagePresenter.instant = true;
$target = params.$target;
targetContent = jQuery(params.$target).html();
$target = params.$target;
targetContent = jQuery(params.$target).html();
pager.init({url: params.backendUri});
setQuery(params.query);
if (forceClear) {
clearContent();
}
pager.init({url: params.backendUri});
setQuery(params.query);
if (forceClear) {
clearContent();
}
promise.wait(util.promiseTemplate('pager'))
.then(function(template) {
templates.pager = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('pager'))
.then(function(template) {
templates.pager = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(params, loaded) {
setQuery(params.query);
if (forceClear) {
clearContent();
}
function reinit(params, loaded) {
setQuery(params.query);
if (forceClear) {
clearContent();
}
promise.wait(retrieve())
.then(loaded)
.fail(loaded);
promise.wait(retrieve())
.then(loaded)
.fail(loaded);
if (!endlessScroll) {
keyboard.keydown('a', function() {
if (pager.prevPage()) {
syncUrl({page: pager.getPage()});
}
});
keyboard.keydown('d', function() {
if (pager.nextPage()) {
syncUrl({page: pager.getPage()});
}
});
}
}
if (!endlessScroll) {
keyboard.keydown(['a', 'left'], navigateToPrevPage);
keyboard.keydown(['d', 'right'], navigateToNextPage);
}
}
function deinit() {
detachNextPageLoader();
}
function deinit() {
detachNextPageLoader();
}
function getUrl(options) {
return util.appendComplexRouteParam(
baseUri,
_.extend(
{},
pager.getSearchParams(),
{page: pager.getPage()},
options));
}
function getUrl(options) {
return util.appendComplexRouteParam(
baseUri,
util.simplifySearchQuery(
_.extend(
{},
pager.getSearchParams(),
{page: pager.getPage()},
options)));
}
function syncUrl(options) {
router.navigate(getUrl(options));
}
function syncUrl(options) {
router.navigate(getUrl(options));
}
function syncUrlInplace(options) {
router.navigateInplace(getUrl(options));
}
function syncUrlInplace(options) {
router.navigateInplace(getUrl(options));
}
function retrieve() {
messagePresenter.hideMessages($messages);
progress.start();
function retrieve() {
messagePresenter.hideMessages($messages);
progress.start();
return promise.make(function(resolve, reject) {
hidePageList();
return promise.make(function(resolve, reject) {
hidePageList();
promise.wait(pager.retrieve())
.then(function(response) {
progress.done();
promise.wait(pager.retrieve())
.then(function(response) {
progress.done();
if (forceClear) {
clearContent();
window.scrollTo(0, 0);
}
var $page = jQuery('<div class="page">');
if (endlessScroll && pager.getTotalPages() > 1) {
$page.append('<p>Page ' + pager.getPage() + ' of ' + pager.getTotalPages() + '</p>');
}
$page.append(targetContent);
$target.find('.pagination-content').append($page);
updateCallback($page, response);
if (forceClear) {
clearContent();
window.scrollTo(0, 0);
}
var $page = jQuery('<div class="page">');
if (endlessScroll && pager.getTotalPages() > 1) {
$page.append('<p>Page ' + pager.getPage() + ' of ' + pager.getTotalPages() + '</p>');
}
$page.append(targetContent);
$target.find('.pagination-content').append($page);
updateCallback($page, response);
refreshPageList();
if (!response.entities.length) {
messagePresenter.showInfo($messages, 'No data to show');
if (pager.getVisiblePages().length === 1) {
hidePageList();
} else {
showPageList();
}
} else {
showPageList();
}
refreshPageList();
if (pager.getPage() < response.totalPages) {
attachNextPageLoader();
}
var entities =
response.json.posts ||
response.json.users ||
response.json.comments ||
response.json.tags ||
response.json.history;
resolve();
}).fail(function(response) {
progress.done();
clearContent();
hidePageList();
messagePresenter.showError($messages, response.json && response.json.error || response);
if (!entities.length) {
messagePresenter.showInfo($messages, 'No data to show');
if (pager.getVisiblePages().length === 1) {
hidePageList();
} else {
showPageList();
}
} else {
showPageList();
}
reject();
});
});
}
if (pager.getPage() < pager.getTotalPages()) {
attachNextPageLoader();
}
function clearContent() {
detachNextPageLoader();
$target.find('.pagination-content').empty();
}
resolve();
}).fail(function(response) {
progress.done();
clearContent();
hidePageList();
messagePresenter.showError($messages, response.json && response.json.error || response);
function attachNextPageLoader() {
if (!endlessScroll) {
return;
}
reject();
});
});
}
detachNextPageLoader();
scrollInterval = window.setInterval(function() {
var myScrollInterval = scrollInterval;
var baseLine = $target.offset().top + $target.innerHeight();
var scrollY = jQuery(window).scrollTop() + jQuery(window).height();
if (scrollY > baseLine) {
syncUrlInplace({page: pager.getPage() + 1});
window.clearInterval(myScrollInterval);
}
}, 100);
}
function clearContent() {
detachNextPageLoader();
$target.find('.pagination-content').empty();
}
function detachNextPageLoader() {
window.clearInterval(scrollInterval);
}
function attachNextPageLoader() {
if (!endlessScroll) {
return;
}
function showPageList() {
$pageList.show();
}
detachNextPageLoader();
scrollInterval = window.setInterval(function() {
var myScrollInterval = scrollInterval;
var baseLine = $target.offset().top + $target.innerHeight();
var scrollY = jQuery(window).scrollTop() + jQuery(window).height();
if (scrollY > baseLine) {
syncUrlInplace({page: pager.getPage() + 1});
window.clearInterval(myScrollInterval);
}
}, 100);
}
function hidePageList() {
$pageList.hide();
}
function detachNextPageLoader() {
window.clearInterval(scrollInterval);
}
function refreshPageList() {
var pages = pager.getVisiblePages();
$pageList.empty();
var lastPage = 0;
_.each(pages, function(page) {
if (page - lastPage > 1) {
$pageList.append(jQuery('<li><a>&hellip;</a></li>'));
}
lastPage = page;
function showPageList() {
$pageList.show();
}
var $a = jQuery('<a href="#"/>');
$a.click(function(e) {
e.preventDefault();
syncUrl({page: page});
});
$a.addClass('big-button');
$a.text(page);
if (page === pager.getPage()) {
$a.addClass('active');
}
var $li = jQuery('<li/>');
$li.append($a);
$pageList.append($li);
});
}
function hidePageList() {
$pageList.hide();
}
function render() {
$target.html(templates.pager());
$messages = $target.find('.pagination-content');
$pageList = $target.find('.page-list');
if (endlessScroll) {
$pageList.remove();
} else {
refreshPageList();
}
}
function navigateToPrevPage() {
console.log('!');
if (pager.prevPage()) {
syncUrl({page: pager.getPage()});
}
}
function setQuery(query) {
if (!query) {
return;
}
query.page = parseInt(query.page) || 1;
var page = query.page;
query = _.extend({}, query);
delete query.page;
forceClear =
query.query !== pager.getSearchParams().query ||
query.order !== pager.getSearchParams().order ||
parseInt(page) !== pager.getPage() + 1 ||
!endlessScroll;
pager.setSearchParams(query);
pager.setPage(page);
}
function navigateToNextPage() {
if (pager.nextPage()) {
syncUrl({page: pager.getPage()});
}
}
function setQueryAndSyncUrl(query) {
setQuery(query);
syncUrl();
}
function refreshPageList() {
var $lastItem = $pageList.find('li:last-child');
var currentPage = pager.getPage();
var pages = pager.getVisiblePages();
$pageList.find('li.page').remove();
var lastPage = 0;
_.each(pages, function(page) {
if (page - lastPage > 1) {
jQuery('<li class="page ellipsis"><a>&hellip;</a></li>').insertBefore($lastItem);
}
lastPage = page;
return {
init: init,
reinit: reinit,
deinit: deinit,
syncUrl: syncUrl,
setQuery: setQueryAndSyncUrl,
};
var $a = jQuery('<a href="#"/>');
$a.click(function(e) {
e.preventDefault();
syncUrl({page: page});
});
$a.addClass('big-button');
$a.text(page);
if (page === currentPage) {
$a.addClass('active');
}
jQuery('<li class="page"/>').append($a).insertBefore($lastItem);
});
$pageList.find('li.next a').unbind('click').bind('click', function(e) {
e.preventDefault();
navigateToNextPage();
});
$pageList.find('li.prev a').unbind('click').bind('click', function(e) {
e.preventDefault();
navigateToPrevPage();
});
}
function render() {
$target.html(templates.pager());
$messages = $target.find('.pagination-content');
$pageList = $target.find('.page-list');
if (endlessScroll) {
$pageList.remove();
} else {
refreshPageList();
}
}
function setQuery(query) {
if (!query) {
return;
}
query.page = parseInt(query.page) || 1;
var page = query.page;
query = _.extend({}, query);
delete query.page;
forceClear =
query.query !== pager.getSearchParams().query ||
query.order !== pager.getSearchParams().order ||
parseInt(page) !== pager.getPage() + 1 ||
!endlessScroll;
pager.setSearchParams(query);
pager.setPage(page);
}
function setQueryAndSyncUrl(query) {
setQuery(query);
syncUrl();
}
return {
init: init,
reinit: reinit,
deinit: deinit,
syncUrl: syncUrl,
setQuery: setQueryAndSyncUrl,
};
};

View File

@ -1,230 +0,0 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PostCommentListPresenter = function(
_,
jQuery,
util,
promise,
api,
auth,
topNavigationPresenter,
messagePresenter) {
var $el;
var privileges;
var templates = {};
var post;
var comments = [];
function init(params, loaded) {
$el = params.$target;
post = params.post;
comments = params.comments || [];
privileges = {
canListComments: auth.hasPrivilege(auth.privileges.listComments),
canAddComments: auth.hasPrivilege(auth.privileges.addComments),
editOwnComments: auth.hasPrivilege(auth.privileges.editOwnComments),
editAllComments: auth.hasPrivilege(auth.privileges.editAllComments),
deleteOwnComments: auth.hasPrivilege(auth.privileges.deleteOwnComments),
deleteAllComments: auth.hasPrivilege(auth.privileges.deleteAllComments),
};
promise.wait(
util.promiseTemplate('post-comment-list'),
util.promiseTemplate('comment-list-item'),
util.promiseTemplate('comment-form'))
.then(function(
commentListTemplate,
commentListItemTemplate,
commentFormTemplate)
{
templates.commentList = commentListTemplate;
templates.commentListItem = commentListItemTemplate;
templates.commentForm = commentFormTemplate;
render();
loaded();
if (comments.length === 0) {
promise.wait(api.get('/comments/' + params.post.id))
.then(function(response) {
comments = response.json.data;
render();
}).fail(function() {
console.log(arguments);
});
}
})
.fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$el.html(templates.commentList(
_.extend(
{
commentListItemTemplate: templates.commentListItem,
commentFormTemplate: templates.commentForm,
formatRelativeTime: util.formatRelativeTime,
formatMarkdown: util.formatMarkdown,
comments: comments,
post: post,
},
privileges)));
$el.find('.comment-add form button[type=submit]').click(function(e) { commentFormSubmitted(e, null); });
renderComments(comments);
}
function renderComments(comments) {
var $target = $el.find('.comments');
var $targetList = $el.find('ul');
if (comments.length > 0) {
$target.show();
} else {
$target.hide();
}
$targetList.empty();
_.each(comments, function(comment) {
renderComment($targetList, comment);
});
}
function renderComment($targetList, comment) {
var $item = jQuery('<li>' + templates.commentListItem({
comment: comment,
formatRelativeTime: util.formatRelativeTime,
formatMarkdown: util.formatMarkdown,
canVote: auth.isLoggedIn(),
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.editOwnComments : privileges.editAllComments,
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.deleteOwnComments : privileges.deleteAllComments,
}) + '</li>');
util.loadImagesNicely($item.find('img'));
$targetList.append($item);
$item.find('a.edit').click(function(e) {
e.preventDefault();
editCommentStart($item, comment);
});
$item.find('a.delete').click(function(e) {
e.preventDefault();
deleteComment(comment);
});
$item.find('a.score-up').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : 1);
});
$item.find('a.score-down').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : -1);
});
}
function commentFormSubmitted(e, comment) {
e.preventDefault();
var $button = jQuery(e.target);
var $form = $button.parents('form');
var sender = $button.val();
if (sender === 'preview') {
previewComment($form);
} else {
submitComment($form, comment);
}
}
function previewComment($form) {
var $preview = $form.find('.preview');
$preview.slideUp('fast', function() {
$preview.html(util.formatMarkdown($form.find('textarea').val()));
$preview.slideDown('fast');
});
}
function updateComment(comment) {
comments = _.map(comments, function(c) { return c.id === comment.id ? comment : c; });
render();
}
function addComment(comment) {
comments.push(comment);
render();
}
function submitComment($form, commentToEdit) {
$form.find('.preview').slideUp();
var $textarea = $form.find('textarea');
var data = {text: $textarea.val()};
var p;
if (commentToEdit) {
p = promise.wait(api.put('/comments/' + commentToEdit.id, data));
} else {
p = promise.wait(api.post('/comments/' + post.id, data));
}
p.then(function(response) {
$textarea.val('');
var comment = response.json;
if (commentToEdit) {
$form.slideUp(function() {
$form.remove();
});
updateComment(comment);
} else {
addComment(comment);
}
}).fail(showGenericError);
}
function editCommentStart($item, comment) {
if ($item.find('.comment-form').length > 0) {
return;
}
var $form = jQuery(templates.commentForm({title: 'Edit comment', text: comment.text}));
$item.find('.body').append($form);
$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
}
function deleteComment(comment) {
if (!window.confirm('Are you sure you want to delete this comment?')) {
return;
}
promise.wait(api.delete('/comments/' + comment.id))
.then(function(response) {
comments = _.filter(comments, function(c) { return c.id !== comment.id; });
renderComments(comments);
}).fail(showGenericError);
}
function score(comment, scoreValue) {
promise.wait(api.post('/comments/' + comment.id + '/score', {score: scoreValue}))
.then(function(response) {
comment.score = parseInt(response.json.score);
comment.ownScore = parseInt(response.json.ownScore);
updateComment(comment);
}).fail(showGenericError);
}
function showGenericError(response) {
window.alert(response.json && response.json.error || response);
}
return {
init: init,
render: render,
};
};
App.DI.register('postCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.PostCommentListPresenter);

View File

@ -2,69 +2,164 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PostContentPresenter = function(
jQuery,
util,
promise,
presenterManager,
postNotesPresenter) {
jQuery,
util,
promise,
keyboard,
presenterManager,
postNotesPresenter,
browsingSettings) {
var post;
var templates = {};
var $target;
var post;
var templates = {};
var $target;
var $wrapper;
function init(params, loaded) {
$target = params.$target;
post = params.post;
function init(params, loaded) {
$target = params.$target;
post = params.post;
promise.wait(util.promiseTemplate('post-content'))
.then(function(postContentTemplate) {
templates.postContent = postContentTemplate;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('post-content'))
.then(function(postContentTemplate) {
templates.postContent = postContentTemplate;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$target.html(templates.postContent({post: post}));
function getFitters() {
var originalWidth = $wrapper.attr('data-width');
var originalHeight = $wrapper.attr('data-height');
var ratio = originalWidth / originalHeight;
var containerHeight = jQuery(window).height() - $wrapper.offset().top - 10;
var containerWidth = $wrapper.parent().outerWidth() - 10;
if (post.contentType === 'image') {
loadPostNotes();
updatePostNotesSize();
}
return {
'fit-both': function(allowUpscale) {
var width = containerWidth;
var height = containerWidth / ratio;
if (height > containerHeight) {
width = containerHeight * ratio;
height = containerHeight;
}
if (!allowUpscale) {
if (width > originalWidth) {
width = originalWidth;
height = originalWidth / ratio;
}
if (height > originalHeight) {
width = originalHeight * ratio;
height = originalHeight;
}
}
$wrapper.css({maxWidth: width + 'px'});
},
'fit-height': function(allowUpscale) {
var width = containerHeight * ratio;
if (width > originalWidth && !allowUpscale) {
width = originalWidth;
}
$wrapper.css({maxWidth: width + 'px'});
},
'fit-width': function(allowUpscale) {
if (allowUpscale) {
$wrapper.css({maxWidth: containerWidth + 'px'});
} else {
$wrapper.css({maxWidth: originalWidth + 'px'});
}
},
'original': function(allowUpscale) {
$wrapper.css({
minWidth: originalWidth + 'px',
width: originalWidth + 'px'});
}
};
}
jQuery(window).resize(updatePostNotesSize);
}
function getFitMode() {
return $wrapper.data('fit-mode');
}
function loadPostNotes() {
presenterManager.initPresenters([
[postNotesPresenter, {post: post, notes: post.notes, $target: $target.find('.post-notes-target')}]],
function() {});
}
function changeFitMode(fitMode) {
$wrapper.data('fit-mode', fitMode);
$wrapper.css({
width: '', height: '',
minWidth: '', minHeight: '',
maxWidth: '', maxHeight: '',
});
getFitters()[fitMode.style](fitMode.upscale);
updatePostNotesSize();
}
function updatePostNotesSize() {
$target.find('.post-notes-target').width($target.find('.image-wrapper').outerWidth());
$target.find('.post-notes-target').height($target.find('.image-wrapper').outerHeight());
}
function cycleFitMode() {
var oldMode = getFitMode();
var fitterNames = Object.keys(getFitters());
var newMode = {
style: fitterNames[(fitterNames.indexOf(oldMode.style) + 1) % fitterNames.length],
upscale: oldMode.upscale,
};
changeFitMode(newMode);
}
function addNewPostNote() {
postNotesPresenter.addNewPostNote();
}
function render() {
$target.html(templates.postContent({post: post}));
$wrapper = $target.find('.object-wrapper');
return {
init: init,
render: render,
addNewPostNote: addNewPostNote,
};
if (post.contentType === 'image' || post.contentType === 'animation') {
loadPostNotes();
updatePostNotesSize();
}
changeFitMode({
style: browsingSettings.getSettings().fitMode,
upscale: browsingSettings.getSettings().upscale,
});
keyboard.keyup('f', cycleFitMode);
jQuery(window).resize(updatePostNotesSize);
}
function loadPostNotes() {
presenterManager.initPresenters([
[postNotesPresenter, {post: post, notes: post.notes, $target: $target.find('.post-notes-target')}]],
function() {});
}
function updatePostNotesSize() {
var $postNotes = $target.find('.post-notes-target');
var $wrapper = $target.find('.object-wrapper');
$postNotes.css({
width: $wrapper.outerWidth() + 'px',
height: $wrapper.outerHeight() + 'px',
left: ($wrapper.offset().left - $wrapper.parent().offset().left) + 'px',
top: ($wrapper.offset().top - $wrapper.parent().offset().top) + 'px',
});
}
function addNewPostNote() {
postNotesPresenter.addNewPostNote();
}
return {
init: init,
render: render,
addNewPostNote: addNewPostNote,
updatePostNotesSize: updatePostNotesSize,
getFitMode: getFitMode,
changeFitMode: changeFitMode,
cycleFitMode: cycleFitMode,
};
};
App.DI.register('postContentPresenter', [
'jQuery',
'util',
'promise',
'presenterManager',
'postNotesPresenter'],
App.Presenters.PostContentPresenter);
'jQuery',
'util',
'promise',
'keyboard',
'presenterManager',
'postNotesPresenter',
'browsingSettings'],
App.Presenters.PostContentPresenter);

View File

@ -2,157 +2,175 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PostEditPresenter = function(
util,
promise,
api,
auth,
tagList) {
jQuery,
util,
promise,
api,
auth,
tagList) {
var $target;
var post;
var updateCallback;
var privileges = {};
var templates = {};
var $target;
var post;
var updateCallback;
var privileges = {};
var templates = {};
var tagInput;
var postContentFileDropper;
var postThumbnailFileDropper;
var postContent;
var postThumbnail;
var tagInput;
var postContentFileDropper;
var postThumbnailFileDropper;
var postContent;
var postThumbnail;
privileges.canChangeSafety = auth.hasPrivilege(auth.privileges.changePostSafety);
privileges.canChangeSource = auth.hasPrivilege(auth.privileges.changePostSource);
privileges.canChangeTags = auth.hasPrivilege(auth.privileges.changePostTags);
privileges.canChangeContent = auth.hasPrivilege(auth.privileges.changePostContent);
privileges.canChangeThumbnail = auth.hasPrivilege(auth.privileges.changePostThumbnail);
privileges.canChangeRelations = auth.hasPrivilege(auth.privileges.changePostRelations);
privileges.canChangeFlags = auth.hasPrivilege(auth.privileges.changePostFlags);
privileges.canChangeSafety = auth.hasPrivilege(auth.privileges.changePostSafety);
privileges.canChangeSource = auth.hasPrivilege(auth.privileges.changePostSource);
privileges.canChangeTags = auth.hasPrivilege(auth.privileges.changePostTags);
privileges.canChangeContent = auth.hasPrivilege(auth.privileges.changePostContent);
privileges.canChangeThumbnail = auth.hasPrivilege(auth.privileges.changePostThumbnail);
privileges.canChangeRelations = auth.hasPrivilege(auth.privileges.changePostRelations);
privileges.canChangeFlags = auth.hasPrivilege(auth.privileges.changePostFlags);
function init(params, loaded) {
post = params.post;
function init(params, loaded) {
post = params.post;
updateCallback = params.updateCallback;
$target = params.$target;
updateCallback = params.updateCallback;
$target = params.$target;
promise.wait(util.promiseTemplate('post-edit'))
.then(function(postEditTemplate) {
templates.postEdit = postEditTemplate;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('post-edit'))
.then(function(postEditTemplate) {
templates.postEdit = postEditTemplate;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$target.html(templates.postEdit({post: post, privileges: privileges}));
function render() {
var $template = jQuery(templates.postEdit({post: post, privileges: privileges}));
postContentFileDropper = new App.Controls.FileDropper($target.find('form [name=content]'));
postContentFileDropper.onChange = postContentChanged;
postContentFileDropper.setNames = true;
postThumbnailFileDropper = new App.Controls.FileDropper($target.find('form [name=thumbnail]'));
postThumbnailFileDropper.onChange = postThumbnailChanged;
postThumbnailFileDropper.setNames = true;
var $advanced = $template.find('.advanced');
var $advancedTrigger = $template.find('.advanced-trigger');
$advanced.hide();
if (!$advanced.length) {
$advancedTrigger.hide();
} else {
$advancedTrigger.find('a').click(function(e) {
advancedTriggerClicked(e, $advanced, $advancedTrigger);
});
}
if (privileges.canChangeTags) {
tagInput = new App.Controls.TagInput($target.find('form [name=tags]'));
tagInput.inputConfirmed = editPost;
}
$target.html($template);
$target.find('form').submit(editFormSubmitted);
}
postContentFileDropper = new App.Controls.FileDropper($target.find('form [name=content]'));
postContentFileDropper.onChange = postContentChanged;
postContentFileDropper.setNames = true;
postThumbnailFileDropper = new App.Controls.FileDropper($target.find('form [name=thumbnail]'));
postThumbnailFileDropper.onChange = postThumbnailChanged;
postThumbnailFileDropper.setNames = true;
function focus() {
if (tagInput) {
tagInput.focus();
}
}
if (privileges.canChangeTags) {
tagInput = new App.Controls.TagInput($target.find('form [name=tags]'));
tagInput.inputConfirmed = editPost;
}
function editFormSubmitted(e) {
e.preventDefault();
editPost();
}
$target.find('form').submit(editFormSubmitted);
}
function postContentChanged(files) {
postContentFileDropper.readAsDataURL(files[0], function(content) {
postContent = content;
});
}
function advancedTriggerClicked(e, $advanced, $advancedTrigger) {
$advancedTrigger.hide();
$advanced.show();
e.preventDefault();
}
function postThumbnailChanged(files) {
postThumbnailFileDropper.readAsDataURL(files[0], function(content) {
postThumbnail = content;
});
}
function focus() {
if (tagInput) {
tagInput.focus();
}
}
function getPrivileges() {
return privileges;
}
function editFormSubmitted(e) {
e.preventDefault();
editPost();
}
function editPost() {
var $form = $target.find('form');
var formData = {};
formData.seenEditTime = post.lastEditTime;
formData.flags = {};
function postContentChanged(files) {
postContent = files[0];
}
if (privileges.canChangeContent && postContent) {
formData.content = postContent;
}
function postThumbnailChanged(files) {
postThumbnail = files[0];
}
if (privileges.canChangeThumbnail && postThumbnail) {
formData.thumbnail = postThumbnail;
}
function getPrivileges() {
return privileges;
}
if (privileges.canChangeSource) {
formData.source = $form.find('[name=source]').val();
}
function editPost() {
var $form = $target.find('form');
var formData = new FormData();
formData.append('lastEditTime', post.lastEditTime);
if (privileges.canChangeSafety) {
formData.safety = $form.find('[name=safety]:checked').val();
}
if (privileges.canChangeContent && postContent) {
formData.append('content', postContent);
}
if (privileges.canChangeTags) {
formData.tags = tagInput.getTags().join(' ');
}
if (privileges.canChangeThumbnail && postThumbnail) {
formData.append('thumbnail', postThumbnail);
}
if (privileges.canChangeRelations) {
formData.relations = $form.find('[name=relations]').val();
}
if (privileges.canChangeSource) {
formData.append('source', $form.find('[name=source]').val());
}
if (privileges.canChangeFlags) {
if (post.contentType === 'video') {
formData.flags.loop = $form.find('[name=loop]').is(':checked') ? 1 : 0;
}
}
if (privileges.canChangeSafety) {
formData.append('safety', $form.find('[name=safety]:checked').val());
}
if (post.tags.length === 0) {
showEditError('No tags set.');
return;
}
if (privileges.canChangeTags) {
formData.append('tags', tagInput.getTags().join(' '));
}
promise.wait(api.put('/posts/' + post.id, formData))
.then(function(response) {
tagList.refreshTags();
if (typeof(updateCallback) !== 'undefined') {
updateCallback(post = response.json);
}
}).fail(function(response) {
showEditError(response);
});
}
if (privileges.canChangeRelations) {
formData.append('relations', $form.find('[name=relations]').val());
}
function showEditError(response) {
window.alert(response.json && response.json.error || response);
}
if (privileges.canChangeFlags) {
if (post.contentType === 'video') {
formData.append('loop', $form.find('[name=loop]').is(':checked') ? 1 : 0);
}
}
return {
init: init,
render: render,
getPrivileges: getPrivileges,
focus: focus,
};
if (post.tags.length === 0) {
showEditError('No tags set.');
return;
}
jQuery(document.activeElement).blur();
promise.wait(api.post('/posts/' + post.id, formData))
.then(function(response) {
tagList.refreshTags();
post = response.json.post;
if (typeof(updateCallback) !== 'undefined') {
updateCallback(post);
}
}).fail(function(response) {
showEditError(response);
});
}
function showEditError(response) {
window.alert(response.json && response.json.error || response);
}
return {
init: init,
render: render,
getPrivileges: getPrivileges,
focus: focus,
};
};
App.DI.register('postEditPresenter', ['util', 'promise', 'api', 'auth', 'tagList'], App.Presenters.PostEditPresenter);
App.DI.register('postEditPresenter', ['jQuery', 'util', 'promise', 'api', 'auth', 'tagList'], App.Presenters.PostEditPresenter);

View File

@ -2,262 +2,264 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PostListPresenter = function(
_,
jQuery,
util,
promise,
auth,
api,
keyboard,
pagerPresenter,
browsingSettings,
topNavigationPresenter) {
_,
jQuery,
util,
promise,
auth,
api,
keyboard,
pagerPresenter,
browsingSettings,
topNavigationPresenter) {
var KEY_RETURN = 13;
var KEY_RETURN = 13;
var templates = {};
var $el = jQuery('#content');
var $searchInput;
var privileges = {};
var templates = {};
var $el = jQuery('#content');
var $searchInput;
var privileges = {};
var params;
var params;
function init(_params, loaded) {
topNavigationPresenter.select('posts');
topNavigationPresenter.changeTitle('Posts');
params = _params;
params.query = params.query || {};
function init(_params, loaded) {
topNavigationPresenter.select('posts');
topNavigationPresenter.changeTitle('Posts');
params = _params;
params.query = params.query || {};
privileges.canMassTag = auth.hasPrivilege(auth.privileges.massTag);
privileges.canMassTag = auth.hasPrivilege(auth.privileges.massTag);
privileges.canViewPosts = auth.hasPrivilege(auth.privileges.viewPosts);
promise.wait(
util.promiseTemplate('post-list'),
util.promiseTemplate('post-list-item'))
.then(function(listTemplate, listItemTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
promise.wait(
util.promiseTemplate('post-list'),
util.promiseTemplate('post-list-item'))
.then(function(listTemplate, listItemTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
render();
loaded();
render();
loaded();
pagerPresenter.init({
baseUri: '#/posts',
backendUri: '/posts',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderPosts($page, data.entities);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
pagerPresenter.init({
baseUri: '#/posts',
backendUri: '/posts',
$target: $el.find('.pagination-target'),
updateCallback: function($page, response) {
renderPosts($page, response.json.posts);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
jQuery(window).on('resize', windowResized);
}
jQuery(window).on('resize', windowResized);
}
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
pagerPresenter.reinit({query: params.query});
loaded();
softRender();
}
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
pagerPresenter.reinit({query: params.query});
loaded();
softRender();
}
function deinit() {
pagerPresenter.deinit();
jQuery(window).off('resize', windowResized);
}
function deinit() {
pagerPresenter.deinit();
jQuery(window).off('resize', windowResized);
}
function render() {
$el.html(templates.list({
massTag: params.query.massTag,
privileges: privileges,
browsingSettings: browsingSettings.getSettings()}));
$searchInput = $el.find('input[name=query]');
App.Controls.AutoCompleteInput($searchInput);
function render() {
$el.html(templates.list({
massTag: params.query.massTag,
privileges: privileges,
browsingSettings: browsingSettings.getSettings()}));
$searchInput = $el.find('input[name=query]');
App.Controls.AutoCompleteInput($searchInput);
$searchInput.val(params.query.query);
$searchInput.keydown(searchInputKeyPressed);
$el.find('form').submit(searchFormSubmitted);
$el.find('[name=mass-tag]').click(massTagButtonClicked);
$el.find('.safety button').click(safetyButtonClicked);
$searchInput.val(params.query.query);
$searchInput.keydown(searchInputKeyPressed);
$el.find('form').submit(searchFormSubmitted);
$el.find('[name=mass-tag]').click(massTagButtonClicked);
$el.find('.safety button').click(safetyButtonClicked);
keyboard.keyup('p', function() {
$el.find('.posts li a').eq(0).focus();
});
keyboard.keyup('p', function() {
$el.find('.posts li a').eq(0).focus();
});
keyboard.keyup('q', function() {
$searchInput.eq(0).focus().select();
});
keyboard.keyup('q', function() {
$searchInput.eq(0).focus().select();
});
windowResized();
}
windowResized();
}
function safetyButtonClicked(e) {
e.preventDefault();
var settings = browsingSettings.getSettings();
var buttonClass = jQuery(e.currentTarget).attr('class').split(' ')[0];
var enabled = jQuery(e.currentTarget).hasClass('disabled');
jQuery(e.currentTarget).toggleClass('disabled');
if (buttonClass === 'safety-unsafe') {
settings.listPosts.unsafe = enabled;
} else if (buttonClass === 'safety-sketchy') {
settings.listPosts.sketchy = enabled;
} else if (buttonClass === 'safety-safe') {
settings.listPosts.safe = enabled;
}
promise.wait(browsingSettings.setSettings(settings))
.then(function() {
reinit(params, function() {});
}).fail(function() {
console.log(arguments);
});
}
function safetyButtonClicked(e) {
e.preventDefault();
var settings = browsingSettings.getSettings();
var buttonClass = jQuery(e.currentTarget).attr('class').split(' ')[0];
var enabled = jQuery(e.currentTarget).hasClass('disabled');
jQuery(e.currentTarget).toggleClass('disabled');
if (buttonClass === 'safety-unsafe') {
settings.listPosts.unsafe = enabled;
} else if (buttonClass === 'safety-sketchy') {
settings.listPosts.sketchy = enabled;
} else if (buttonClass === 'safety-safe') {
settings.listPosts.safe = enabled;
}
promise.wait(browsingSettings.setSettings(settings))
.then(function() {
reinit(params, function() {});
}).fail(function() {
console.log(arguments);
});
}
function softRender() {
$searchInput.val(params.query.query);
function softRender() {
$searchInput.val(params.query.query);
var $massTagInfo = $el.find('.mass-tag-info');
if (params.query.massTag) {
$massTagInfo.show();
$massTagInfo.find('span').text(params.query.massTag);
} else {
$massTagInfo.hide();
}
_.map($el.find('.posts .post-small'), function(postNode) { softRenderPost(jQuery(postNode).parents('li')); });
}
var $massTagInfo = $el.find('.mass-tag-info');
if (params.query.massTag) {
$massTagInfo.show();
$massTagInfo.find('span').text(params.query.massTag);
} else {
$massTagInfo.hide();
}
_.map($el.find('.posts .post-small'), function(postNode) { softRenderPost(jQuery(postNode).parents('li')); });
}
function renderPosts($page, posts) {
var $target = $page.find('.posts');
_.each(posts, function(post) {
if (!shouldSkipPost(post)) {
var $post = renderPost(post);
softRenderPost($post);
$target.append($post);
}
});
windowResized();
}
function renderPosts($page, posts) {
var $target = $page.find('.posts');
_.each(posts, function(post) {
if (!shouldSkipPost(post)) {
var $post = renderPost(post);
softRenderPost($post);
$target.append($post);
}
});
windowResized();
}
function shouldSkipPost(post) {
var settings = browsingSettings.getSettings();
if (post.ownScore < 0 && settings.hideDownvoted) {
return true;
}
if (settings.listPosts) {
if (post.safety === 'safe' && !settings.listPosts.safe) {
return true;
} else if (post.safety === 'sketchy' && !settings.listPosts.sketchy) {
return true;
} else if (post.safety === 'unsafe' && !settings.listPosts.unsafe) {
return true;
}
}
return false;
}
function shouldSkipPost(post) {
var settings = browsingSettings.getSettings();
if (post.ownScore < 0 && settings.hideDownvoted) {
return true;
}
if (settings.listPosts) {
if (post.safety === 'safe' && !settings.listPosts.safe) {
return true;
} else if (post.safety === 'sketchy' && !settings.listPosts.sketchy) {
return true;
} else if (post.safety === 'unsafe' && !settings.listPosts.unsafe) {
return true;
}
}
return false;
}
function renderPost(post) {
var $post = jQuery('<li>' + templates.listItem({
util: util,
query: params.query,
post: post,
}) + '</li>');
$post.data('post', post);
util.loadImagesNicely($post.find('img'));
return $post;
}
function renderPost(post) {
var $post = jQuery('<li>' + templates.listItem({
util: util,
query: params.query,
post: post,
canViewPosts: privileges.canViewPosts,
}) + '</li>');
$post.data('post', post);
util.loadImagesNicely($post.find('img'));
return $post;
}
function softRenderPost($post) {
var classes = [];
if (params.query.massTag) {
var post = $post.data('post');
if (_.contains(_.map(post.tags, function(tag) { return tag.name.toLowerCase(); }), params.query.massTag.toLowerCase())) {
classes.push('tagged');
} else {
classes.push('untagged');
}
}
$post.toggleClass('tagged', _.contains(classes, 'tagged'));
$post.toggleClass('untagged', _.contains(classes, 'untagged'));
$post.find('.action').toggle(_.any(classes));
$post.find('.action button').text(_.contains(classes, 'tagged') ? 'Tagged' : 'Untagged').unbind('click').click(postTagButtonClicked);
}
function softRenderPost($post) {
var classes = [];
if (params.query.massTag) {
var post = $post.data('post');
if (_.contains(_.map(post.tags, function(tag) { return tag.name.toLowerCase(); }), params.query.massTag.toLowerCase())) {
classes.push('tagged');
} else {
classes.push('untagged');
}
}
$post.toggleClass('tagged', _.contains(classes, 'tagged'));
$post.toggleClass('untagged', _.contains(classes, 'untagged'));
$post.find('.action').toggle(_.any(classes));
$post.find('.action button').text(_.contains(classes, 'tagged') ? 'Tagged' : 'Untagged').unbind('click').click(postTagButtonClicked);
}
function windowResized() {
var $list = $el.find('ul.posts');
var $posts = $list.find('.post-small');
var $firstPost = $posts.eq(0);
var $lastPost = $firstPost;
for (var i = 1; i < $posts.length; i ++) {
$lastPost = $posts.eq(i-1);
if ($posts.eq(i).offset().left < $lastPost.offset().left) {
break;
}
}
if ($firstPost.length === 0) {
return;
}
$el.find('.search').width($lastPost.offset().left + $lastPost.width() - $firstPost.offset().left);
}
function windowResized() {
var $list = $el.find('ul.posts');
var $posts = $list.find('.post-small');
var $firstPost = $posts.eq(0);
var $lastPost = $firstPost;
for (var i = 1; i < $posts.length; i ++) {
$lastPost = $posts.eq(i-1);
if ($posts.eq(i).offset().left < $lastPost.offset().left) {
break;
}
}
if ($firstPost.length === 0) {
return;
}
$el.find('.search').width($lastPost.offset().left + $lastPost.width() - $firstPost.offset().left);
}
function postTagButtonClicked(e) {
e.preventDefault();
var $post = jQuery(e.target).parents('li');
var post = $post.data('post');
var tags = _.pluck(post.tags, 'name');
if (_.contains(_.map(tags, function(tag) { return tag.toLowerCase(); }), params.query.massTag.toLowerCase())) {
tags = _.filter(tags, function(tag) { return tag.toLowerCase() !== params.query.massTag.toLowerCase(); });
} else {
tags.push(params.query.massTag);
}
var formData = {};
formData.seenEditTime = post.lastEditTime;
formData.tags = tags.join(' ');
promise.wait(api.put('/posts/' + post.id, formData))
.then(function(response) {
post = response.json;
$post.data('post', post);
softRenderPost($post);
}).fail(function(response) {
window.alert(response.json && response.json.error || response);
});
}
function postTagButtonClicked(e) {
e.preventDefault();
var $post = jQuery(e.target).parents('li');
var post = $post.data('post');
var tags = _.pluck(post.tags, 'name');
if (_.contains(_.map(tags, function(tag) { return tag.toLowerCase(); }), params.query.massTag.toLowerCase())) {
tags = _.filter(tags, function(tag) { return tag.toLowerCase() !== params.query.massTag.toLowerCase(); });
} else {
tags.push(params.query.massTag);
}
var formData = {};
formData.lastEditTime = post.lastEditTime;
formData.tags = tags.join(' ');
promise.wait(api.post('/posts/' + post.id, formData))
.then(function(response) {
post = response.json.post;
$post.data('post', post);
softRenderPost($post);
}).fail(function(response) {
window.alert(response.json && response.json.error || response);
});
}
function searchInputKeyPressed(e) {
if (e.which !== KEY_RETURN) {
return;
}
updateSearch();
}
function searchInputKeyPressed(e) {
if (e.which !== KEY_RETURN) {
return;
}
updateSearch();
}
function massTagButtonClicked(e) {
e.preventDefault();
params.query.massTag = window.prompt('Enter tag to tag with:');
pagerPresenter.setQuery(params.query);
}
function massTagButtonClicked(e) {
e.preventDefault();
params.query.massTag = window.prompt('Enter tag to tag with:');
pagerPresenter.setQuery(params.query);
}
function searchFormSubmitted(e) {
e.preventDefault();
updateSearch();
}
function searchFormSubmitted(e) {
e.preventDefault();
updateSearch();
}
function updateSearch() {
$searchInput.blur();
params.query.query = $searchInput.val().trim();
params.query.page = 1;
pagerPresenter.setQuery(params.query);
}
function updateSearch() {
$searchInput.blur();
params.query.query = $searchInput.val().trim();
params.query.page = 1;
pagerPresenter.setQuery(params.query);
}
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
};

View File

@ -2,192 +2,209 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PostNotesPresenter = function(
jQuery,
util,
promise,
api,
auth,
draggable,
resizable) {
jQuery,
util,
promise,
api,
auth,
draggable,
resizable) {
var post;
var notes;
var templates = {};
var $target;
var $form;
var privileges = {};
var post;
var notes;
var templates = {};
var $target;
var $form;
var privileges = {};
function init(params, loaded) {
$target = params.$target;
post = params.post;
notes = params.notes || [];
function init(params, loaded) {
$target = params.$target;
post = params.post;
notes = params.notes || [];
privileges.canDeletePostNotes = auth.hasPrivilege(auth.privileges.deletePostNotes);
privileges.canEditPostNotes = auth.hasPrivilege(auth.privileges.editPostNotes);
privileges.canDeletePostNotes = auth.hasPrivilege(auth.privileges.deletePostNotes);
privileges.canEditPostNotes = auth.hasPrivilege(auth.privileges.editPostNotes);
promise.wait(util.promiseTemplate('post-notes'))
.then(function(postNotesTemplate) {
templates.postNotes = postNotesTemplate;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('post-notes'))
.then(function(postNotesTemplate) {
templates.postNotes = postNotesTemplate;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function addNewPostNote() {
notes.push({left: 10.0, top: 10.0, width: 10.0, height: 10.0, text: '…'});
}
function addNewPostNote() {
notes.push({left: 10.0, top: 10.0, width: 10.0, height: 10.0, text: '…'});
}
function addNewPostNoteAndRender() {
addNewPostNote();
render();
}
function addNewPostNoteAndRender() {
addNewPostNote();
render();
}
function render() {
$target.html(templates.postNotes({
privileges: privileges,
post: post,
notes: notes,
formatMarkdown: util.formatMarkdown}));
function render() {
$target.html(templates.postNotes({
privileges: privileges,
post: post,
notes: notes,
util: util}));
$form = $target.find('.post-note-edit');
var $postNotes = $target.find('.post-note');
$form = $target.find('.post-note-edit');
var $postNotes = $target.find('.post-note');
$postNotes.each(function(i) {
var postNote = notes[i];
var $postNote = jQuery(this);
$postNote.data('postNote', postNote);
$postNote.find('.text-wrapper').click(postNoteClicked);
postNote.$element = $postNote;
draggable.makeDraggable($postNote, draggable.relativeDragStrategy);
resizable.makeResizable($postNote);
});
$postNotes.each(function(i) {
var postNote = notes[i];
var $postNote = jQuery(this);
$postNote.data('postNote', postNote);
$postNote.find('.text-wrapper').click(postNoteClicked);
postNote.$element = $postNote;
draggable.makeDraggable($postNote, draggable.relativeDragStrategy, true);
resizable.makeResizable($postNote, true);
$postNote.mouseenter(function() { postNoteMouseEnter(postNote); });
$postNote.mouseleave(function() { postNoteMouseLeave(postNote); });
});
$form.find('button').click(formSubmitted);
}
$form.find('button').click(formSubmitted);
}
function formSubmitted(e) {
e.preventDefault();
var $button = jQuery(e.target);
var sender = $button.val();
function formSubmitted(e) {
e.preventDefault();
var $button = jQuery(e.target);
var sender = $button.val();
var postNote = $form.data('postNote');
postNote.left = (postNote.$element.offset().left - $target.offset().left) * 100.0 / $target.outerWidth();
postNote.top = (postNote.$element.offset().top - $target.offset().top) * 100.0 / $target.outerHeight();
postNote.width = postNote.$element.width() * 100.0 / $target.outerWidth();
postNote.height = postNote.$element.height() * 100.0 / $target.outerHeight();
postNote.text = $form.find('textarea').val();
var postNote = $form.data('postNote');
postNote.left = (postNote.$element.offset().left - $target.offset().left) * 100.0 / $target.outerWidth();
postNote.top = (postNote.$element.offset().top - $target.offset().top) * 100.0 / $target.outerHeight();
postNote.width = postNote.$element.width() * 100.0 / $target.outerWidth();
postNote.height = postNote.$element.height() * 100.0 / $target.outerHeight();
postNote.text = $form.find('textarea').val();
if (sender === 'cancel') {
hideForm();
} else if (sender === 'remove') {
removePostNote(postNote);
} else if (sender === 'save') {
savePostNote(postNote);
} else if (sender === 'preview') {
previewPostNote(postNote);
}
}
if (sender === 'cancel') {
hideForm();
} else if (sender === 'remove') {
removePostNote(postNote);
} else if (sender === 'save') {
savePostNote(postNote);
} else if (sender === 'preview') {
previewPostNote(postNote);
}
}
function removePostNote(postNote) {
if (postNote.id) {
if (window.confirm('Are you sure you want to delete this note?')) {
promise.wait(api.delete('/notes/' + postNote.id))
.then(function() {
hideForm();
postNote.$element.remove();
}).fail(function(response) {
window.alert(response.json && response.json.error || response);
});
}
} else {
postNote.$element.remove();
hideForm();
}
}
function removePostNote(postNote) {
if (postNote.id) {
if (window.confirm('Are you sure you want to delete this note?')) {
promise.wait(api.delete('/notes/' + postNote.id))
.then(function() {
hideForm();
notes = jQuery.grep(notes, function(otherNote) {
return otherNote.id !== postNote.id;
});
render();
}).fail(function(response) {
window.alert(response.json && response.json.error || response);
});
}
} else {
postNote.$element.remove();
hideForm();
}
}
function savePostNote(postNote) {
if (window.confirm('Are you sure you want to save this note?')) {
var formData = {
left: postNote.left,
top: postNote.top,
width: postNote.width,
height: postNote.height,
text: postNote.text,
};
function savePostNote(postNote) {
if (window.confirm('Are you sure you want to save this note?')) {
var formData = {
left: postNote.left,
top: postNote.top,
width: postNote.width,
height: postNote.height,
text: postNote.text,
};
var p = postNote.id ?
api.put('/notes/' + postNote.id, formData) :
api.post('/notes/' + post.id, formData);
var p = postNote.id ?
api.put('/notes/' + postNote.id, formData) :
api.post('/notes/' + post.id, formData);
promise.wait(p)
.then(function(response) {
hideForm();
postNote.id = response.json.id;
postNote.$element.data('postNote', postNote);
render();
}).fail(function(response) {
window.alert(response.json && response.json.error || response);
});
}
}
promise.wait(p)
.then(function(response) {
hideForm();
postNote.id = response.json.note.id;
postNote.$element.data('postNote', postNote);
render();
}).fail(function(response) {
window.alert(response.json && response.json.error || response);
});
}
}
function previewPostNote(postNote) {
var previewText = $form.find('textarea').val();
postNote.$element.find('.text').html(util.formatMarkdown(previewText));
showPostNoteText(postNote);
}
function previewPostNote(postNote) {
var previewText = $form.find('textarea').val();
postNote.$element.find('.text').html(util.formatMarkdown(previewText));
showPostNoteText(postNote);
}
function showPostNoteText(postNote) {
postNote.$element.find('.text-wrapper').show();
}
function showPostNoteText(postNote) {
var $textWrapper = postNote.$element.find('.text-wrapper');
$textWrapper.show();
if ($textWrapper.offset().left + $textWrapper.width() > jQuery(window).outerWidth()) {
$textWrapper.offset({left: jQuery(window).outerWidth() - $textWrapper.width()});
}
}
function hidePostNoteText(postNote) {
postNote.$element.find('.text-wrapper').css('display', '');
}
function hidePostNoteText(postNote) {
postNote.$element.find('.text-wrapper').css('display', '');
}
function postNoteClicked(e) {
e.preventDefault();
var $postNote = jQuery(e.currentTarget).parents('.post-note');
if ($postNote.hasClass('resizing') || $postNote.hasClass('dragging')) {
return;
}
showFormForPostNote($postNote);
}
function postNoteMouseEnter(postNote) {
showPostNoteText(postNote);
}
function showFormForPostNote($postNote) {
hideForm();
var postNote = $postNote.data('postNote');
$form.data('postNote', postNote);
$form.find('textarea').val(postNote.text);
$form.show();
draggable.makeDraggable($form, draggable.absoluteDragStrategy);
}
function postNoteMouseLeave(postNote) {
hidePostNoteText(postNote);
}
function hideForm() {
var previousPostNote = $form.data('post-note');
if (previousPostNote) {
hidePostNoteText(previousPostNote);
}
$form.hide();
}
function postNoteClicked(e) {
e.preventDefault();
var $postNote = jQuery(e.currentTarget).parents('.post-note');
if ($postNote.hasClass('resizing') || $postNote.hasClass('dragging')) {
return;
}
showFormForPostNote($postNote);
}
return {
init: init,
render: render,
addNewPostNote: addNewPostNoteAndRender,
};
function showFormForPostNote($postNote) {
hideForm();
var postNote = $postNote.data('postNote');
$form.data('postNote', postNote);
$form.find('textarea').val(postNote.text);
$form.show();
draggable.makeDraggable($form, draggable.absoluteDragStrategy, false);
}
function hideForm() {
var previousPostNote = $form.data('post-note');
if (previousPostNote) {
hidePostNoteText(previousPostNote);
}
$form.hide();
}
return {
init: init,
render: render,
addNewPostNote: addNewPostNoteAndRender,
};
};
App.DI.register('postNotesPresenter', [
'jQuery',
'util',
'promise',
'api',
'auth',
'draggable',
'resizable'],
App.Presenters.PostNotesPresenter);
'jQuery',
'util',
'promise',
'api',
'auth',
'draggable',
'resizable'],
App.Presenters.PostNotesPresenter);

View File

@ -2,338 +2,362 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PostPresenter = function(
_,
jQuery,
util,
promise,
api,
auth,
router,
keyboard,
presenterManager,
postsAroundCalculator,
postEditPresenter,
postContentPresenter,
postCommentListPresenter,
topNavigationPresenter,
messagePresenter) {
_,
jQuery,
appState,
util,
promise,
api,
auth,
router,
keyboard,
presenterManager,
postsAroundCalculator,
postEditPresenter,
postContentPresenter,
commentListPresenter,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content');
var $messages = $el;
var $el = jQuery('#content');
var $messages = $el;
var templates = {};
var params;
var templates = {};
var params;
var postNameOrId;
var post;
var postNameOrId;
var post;
var privileges = {};
var privileges = {};
function init(params, loaded) {
topNavigationPresenter.select('posts');
postsAroundCalculator.resetCache();
function init(params, loaded) {
topNavigationPresenter.select('posts');
postsAroundCalculator.resetCache();
privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts);
privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts);
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
privileges.canAddPostNotes = auth.hasPrivilege(auth.privileges.addPostNotes);
privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts);
privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts);
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
privileges.canAddPostNotes = auth.hasPrivilege(auth.privileges.addPostNotes);
promise.wait(
util.promiseTemplate('post'),
util.promiseTemplate('history'))
.then(function(
postTemplate,
historyTemplate) {
templates.post = postTemplate;
templates.history = historyTemplate;
promise.wait(
util.promiseTemplate('post'),
util.promiseTemplate('history'))
.then(function(
postTemplate,
historyTemplate) {
templates.post = postTemplate;
templates.history = historyTemplate;
reinit(params, loaded);
}).fail(function(response) {
showGenericError(response);
loaded();
});
}
reinit(params, loaded);
}).fail(function(response) {
showGenericError(response);
loaded();
});
}
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
params.query.page = parseInt(params.query.page) || 1;
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
params.query.page = parseInt(params.query.page) || 1;
postNameOrId = params.postNameOrId;
postNameOrId = params.postNameOrId;
promise.wait(refreshPost())
.then(function() {
topNavigationPresenter.changeTitle('@' + post.id);
render();
loaded();
promise.wait(refreshPost())
.then(function() {
topNavigationPresenter.changeTitle('@' + post.id);
render();
loaded();
presenterManager.initPresenters([
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}],
[postEditPresenter, {post: post, $target: $el.find('#post-edit-target'), updateCallback: postEdited}],
[postCommentListPresenter, {post: post, $target: $el.find('#post-comments-target')}]],
function() { });
presenterManager.initPresenters([
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}],
[postEditPresenter, {post: post, $target: $el.find('#post-edit-target'), updateCallback: postEdited}],
[commentListPresenter, {post: post, $target: $el.find('#post-comments-target')}]],
function() {
syncFitModeButtons();
});
}).fail(function() {
console.log(arguments);
loaded();
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
}
function attachLinksToPostsAround() {
promise.wait(postsAroundCalculator.getLinksToPostsAround(params.query, post.id))
.then(function(nextPostUrl, prevPostUrl) {
var $prevPost = $el.find('#post-current-search .right a');
var $nextPost = $el.find('#post-current-search .left a');
function attachLinksToPostsAround() {
promise.wait(postsAroundCalculator.getLinksToPostsAround(params.query, post.id))
.then(function(nextPostUrl, prevPostUrl) {
var $prevPost = $el.find('#post-current-search .right a');
var $nextPost = $el.find('#post-current-search .left a');
if (nextPostUrl) {
$nextPost.addClass('enabled');
$nextPost.attr('href', nextPostUrl);
keyboard.keyup('a', function() {
router.navigate(nextPostUrl);
});
} else {
$nextPost.removeClass('enabled');
$nextPost.removeAttr('href');
keyboard.unbind('a');
}
if (nextPostUrl) {
$nextPost.addClass('enabled');
$nextPost.attr('href', nextPostUrl);
keyboard.keyup(['a', 'left'], function() {
router.navigate(nextPostUrl);
});
} else {
$nextPost.removeClass('enabled');
$nextPost.removeAttr('href');
keyboard.unbind(['a', 'left']);
}
if (prevPostUrl) {
$prevPost.addClass('enabled');
$prevPost.attr('href', prevPostUrl);
keyboard.keyup('d', function() {
router.navigate(prevPostUrl);
});
} else {
$prevPost.removeClass('enabled');
$prevPost.removeAttr('href');
keyboard.unbind('d');
}
}).fail(function() {
});
}
if (prevPostUrl) {
$prevPost.addClass('enabled');
$prevPost.attr('href', prevPostUrl);
keyboard.keyup(['d', 'right'], function() {
router.navigate(prevPostUrl);
});
} else {
$prevPost.removeClass('enabled');
$prevPost.removeAttr('href');
keyboard.unbind(['d', 'right']);
}
}).fail(function() {
});
}
function refreshPost() {
return promise.make(function(resolve, reject) {
promise.wait(api.get('/posts/' + postNameOrId))
.then(function(postResponse) {
post = postResponse.json;
resolve();
}).fail(function(response) {
showGenericError(response);
reject();
});
});
}
function refreshPost() {
return promise.make(function(resolve, reject) {
promise.wait(api.get('/posts/' + postNameOrId))
.then(function(postResponse) {
post = postResponse.json.post;
resolve();
}).fail(function(response) {
showGenericError(response);
reject();
});
});
}
function render() {
$el.html(renderPostTemplate());
$messages = $el.find('.messages');
function render() {
$el.html(renderPostTemplate());
$messages = $el.find('.messages');
keyboard.keyup('e', function() {
editButtonClicked(null);
});
keyboard.keyup('e', function() {
editButtonClicked(null);
});
attachSidebarEvents();
attachSidebarEvents();
attachLinksToPostsAround();
}
attachLinksToPostsAround();
}
function postEdited(newPost) {
post = newPost;
hideEditForm();
softRender();
}
function postEdited(newPost) {
post = newPost;
hideEditForm();
softRender();
}
function softRender() {
renderSidebar();
syncFitModeButtons();
$el.find('video').prop('loop', post.flags.loop);
}
function softRender() {
renderSidebar();
$el.find('video').prop('loop', post.flags.loop);
}
function renderSidebar() {
$el.find('#sidebar').html(jQuery(renderPostTemplate()).find('#sidebar').html());
attachSidebarEvents();
}
function renderSidebar() {
$el.find('#sidebar').html(jQuery(renderPostTemplate()).find('#sidebar').html());
attachSidebarEvents();
}
function renderPostTemplate() {
return templates.post({
query: params.query,
post: post,
forceHttpInPermalinks: appState.get('config').forceHttpInPermalinks,
ownScore: post.ownScore,
postFavorites: post.favorites,
postHistory: post.history,
function renderPostTemplate() {
return templates.post({
query: params.query,
post: post,
ownScore: post.ownScore,
postFavorites: post.favorites,
postHistory: post.history,
util: util,
formatRelativeTime: util.formatRelativeTime,
formatFileSize: util.formatFileSize,
historyTemplate: templates.history,
historyTemplate: templates.history,
hasFav: _.any(post.favorites, function(favUser) { return favUser.id === auth.getCurrentUser().id; }),
isLoggedIn: auth.isLoggedIn(),
privileges: privileges,
editPrivileges: postEditPresenter.getPrivileges(),
});
}
hasFav: _.any(post.favorites, function(favUser) { return favUser.id === auth.getCurrentUser().id; }),
isLoggedIn: auth.isLoggedIn(),
privileges: privileges,
editPrivileges: postEditPresenter.getPrivileges(),
});
}
function attachSidebarEvents() {
$el.find('#sidebar .delete').click(deleteButtonClicked);
$el.find('#sidebar .feature').click(featureButtonClicked);
$el.find('#sidebar .fit-mode a').click(fitModeButtonsClicked);
$el.find('#sidebar .edit').click(editButtonClicked);
$el.find('#sidebar .history').click(historyButtonClicked);
$el.find('#sidebar .add-favorite').click(addFavoriteButtonClicked);
$el.find('#sidebar .delete-favorite').click(deleteFavoriteButtonClicked);
$el.find('#sidebar .score-up').click(scoreUpButtonClicked);
$el.find('#sidebar .score-down').click(scoreDownButtonClicked);
$el.find('#sidebar .add-note').click(addNoteButtonClicked);
}
function attachSidebarEvents() {
$el.find('#sidebar .delete').click(deleteButtonClicked);
$el.find('#sidebar .feature').click(featureButtonClicked);
$el.find('#sidebar .edit').click(editButtonClicked);
$el.find('#sidebar .history').click(historyButtonClicked);
$el.find('#sidebar .add-favorite').click(addFavoriteButtonClicked);
$el.find('#sidebar .delete-favorite').click(deleteFavoriteButtonClicked);
$el.find('#sidebar .score-up').click(scoreUpButtonClicked);
$el.find('#sidebar .score-down').click(scoreDownButtonClicked);
$el.find('#sidebar .add-note').click(addNoteButtonClicked);
}
function addNoteButtonClicked(e) {
e.preventDefault();
postContentPresenter.addNewPostNote();
}
function addNoteButtonClicked(e) {
e.preventDefault();
postContentPresenter.addNewPostNote();
}
function deleteButtonClicked(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
if (window.confirm('Do you really want to delete this post?')) {
deletePost();
}
}
function deleteButtonClicked(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
if (window.confirm('Do you really want to delete this post?')) {
deletePost();
}
}
function deletePost() {
promise.wait(api.delete('/posts/' + post.id))
.then(function(response) {
router.navigate('#/posts');
}).fail(showGenericError);
}
function deletePost() {
promise.wait(api.delete('/posts/' + post.id))
.then(function(response) {
router.navigate('#/posts');
}).fail(showGenericError);
}
function syncFitModeButtons() {
var fitStyle = postContentPresenter.getFitMode().style;
$el.find('#sidebar .fit-mode a').each(function(i, item) {
var $item = jQuery(item);
$item.toggleClass('active', $item.attr('data-fit-mode') === fitStyle);
});
}
function featureButtonClicked(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
if (window.confirm('Do you want to feature this post on the front page?')) {
featurePost();
}
}
function featureButtonClicked(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
if (window.confirm('Do you want to feature this post on the front page?')) {
featurePost();
}
}
function featurePost() {
promise.wait(api.post('/posts/' + post.id + '/feature'))
.then(function(response) {
router.navigate('#/home');
}).fail(showGenericError);
}
function fitModeButtonsClicked(e) {
e.preventDefault();
var oldMode = postContentPresenter.getFitMode();
var newMode = {
style: jQuery(e.target).attr('data-fit-mode'),
upscale: oldMode.upscale,
};
postContentPresenter.changeFitMode(newMode);
syncFitModeButtons();
}
function editButtonClicked(e) {
if (e) {
e.preventDefault();
}
messagePresenter.hideMessages($messages);
if ($el.find('#post-edit-target').is(':visible')) {
hideEditForm();
} else {
showEditForm();
}
}
function featurePost() {
promise.wait(api.post('/posts/' + post.id + '/feature'))
.then(function(response) {
router.navigate('#/home');
}).fail(showGenericError);
}
function showEditForm() {
$el.find('#post-edit-target').slideDown('fast');
util.enableExitConfirmation();
postEditPresenter.focus();
}
function editButtonClicked(e) {
if (e) {
e.preventDefault();
}
messagePresenter.hideMessages($messages);
if ($el.find('#post-edit-target').is(':visible')) {
hideEditForm();
} else {
showEditForm();
}
}
function hideEditForm() {
$el.find('#post-edit-target').slideUp('fast');
util.disableExitConfirmation();
}
function showEditForm() {
$el.find('#post-edit-target').slideDown('fast');
util.enableExitConfirmation();
postEditPresenter.focus();
}
function historyButtonClicked(e) {
e.preventDefault();
if ($el.find('.post-history-wrapper').is(':visible')) {
hideHistory();
} else {
showHistory();
}
}
function hideEditForm() {
$el.find('#post-edit-target').slideUp('fast');
util.disableExitConfirmation();
}
function hideHistory() {
$el.find('.post-history-wrapper').slideUp('slow');
}
function historyButtonClicked(e) {
e.preventDefault();
if ($el.find('.post-history-wrapper').is(':visible')) {
hideHistory();
} else {
showHistory();
}
}
function showHistory() {
$el.find('.post-history-wrapper').slideDown('slow');
}
function hideHistory() {
$el.find('.post-history-wrapper').slideUp('slow');
}
function addFavoriteButtonClicked(e) {
e.preventDefault();
addFavorite();
}
function showHistory() {
$el.find('.post-history-wrapper').slideDown('slow');
}
function deleteFavoriteButtonClicked(e) {
e.preventDefault();
deleteFavorite();
}
function addFavoriteButtonClicked(e) {
e.preventDefault();
addFavorite();
}
function addFavorite() {
promise.wait(api.post('/posts/' + post.id + '/favorites'))
.then(function(response) {
promise.wait(refreshPost()).then(softRender);
}).fail(showGenericError);
}
function deleteFavoriteButtonClicked(e) {
e.preventDefault();
deleteFavorite();
}
function deleteFavorite() {
promise.wait(api.delete('/posts/' + post.id + '/favorites'))
.then(function(response) {
promise.wait(refreshPost()).then(softRender);
}).fail(showGenericError);
}
function addFavorite() {
promise.wait(api.post('/posts/' + post.id + '/favorites'))
.then(function(response) {
promise.wait(refreshPost()).then(softRender);
}).fail(showGenericError);
}
function scoreUpButtonClicked(e) {
e.preventDefault();
var $target = jQuery(this);
score($target.hasClass('active') ? 0 : 1);
}
function deleteFavorite() {
promise.wait(api.delete('/posts/' + post.id + '/favorites'))
.then(function(response) {
promise.wait(refreshPost()).then(softRender);
}).fail(showGenericError);
}
function scoreDownButtonClicked(e) {
e.preventDefault();
var $target = jQuery(this);
score($target.hasClass('active') ? 0 : -1);
}
function scoreUpButtonClicked(e) {
e.preventDefault();
var $target = jQuery(this);
score($target.hasClass('active') ? 0 : 1);
}
function score(scoreValue) {
promise.wait(api.post('/posts/' + post.id + '/score', {score: scoreValue}))
.then(function() {
promise.wait(refreshPost()).then(softRender);
}).fail(showGenericError);
}
function scoreDownButtonClicked(e) {
e.preventDefault();
var $target = jQuery(this);
score($target.hasClass('active') ? 0 : -1);
}
function showGenericError(response) {
if ($messages === $el) {
$el.empty();
}
messagePresenter.showError($messages, response.json && response.json.error || response);
}
function score(scoreValue) {
promise.wait(api.post('/posts/' + post.id + '/score', {score: scoreValue}))
.then(function() {
promise.wait(refreshPost()).then(softRender);
}).fail(showGenericError);
}
return {
init: init,
reinit: reinit,
render: render
};
function showGenericError(response) {
if ($messages === $el) {
$el.empty();
}
messagePresenter.showError($messages, response.json && response.json.error || response);
}
return {
init: init,
reinit: reinit,
render: render
};
};
App.DI.register('postPresenter', [
'_',
'jQuery',
'util',
'promise',
'api',
'auth',
'router',
'keyboard',
'presenterManager',
'postsAroundCalculator',
'postEditPresenter',
'postContentPresenter',
'postCommentListPresenter',
'topNavigationPresenter',
'messagePresenter'],
App.Presenters.PostPresenter);
'_',
'jQuery',
'appState',
'util',
'promise',
'api',
'auth',
'router',
'keyboard',
'presenterManager',
'postsAroundCalculator',
'postEditPresenter',
'postContentPresenter',
'commentListPresenter',
'topNavigationPresenter',
'messagePresenter'],
App.Presenters.PostPresenter);

File diff suppressed because it is too large Load Diff

View File

@ -2,43 +2,43 @@ var App = App || {};
App.Controls = App.Controls || {};
App.Presenters.ProgressPresenter = function(nprogress) {
var nesting = 0;
var nesting = 0;
function start() {
nesting ++;
function start() {
nesting ++;
if (nesting === 1) {
nprogress.start();
}
}
if (nesting === 1) {
nprogress.start();
}
}
function reset() {
nesting = 0;
}
function reset() {
nesting = 0;
}
function done() {
if (nesting) {
nesting --;
}
function done() {
if (nesting) {
nesting --;
}
if (nesting <= 0) {
nprogress.done();
} else {
nprogress.inc();
}
}
if (nesting <= 0) {
nprogress.done();
} else {
nprogress.inc();
}
}
window.setInterval(function() {
if (nesting <= 0) {
nprogress.done();
}
}, 1000);
window.setInterval(function() {
if (nesting <= 0) {
nprogress.done();
}
}, 1000);
return {
start: start,
done: done,
reset: reset,
};
return {
start: start,
done: done,
reset: reset,
};
};

View File

@ -2,100 +2,100 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.RegistrationPresenter = function(
jQuery,
util,
promise,
api,
topNavigationPresenter,
messagePresenter) {
jQuery,
util,
promise,
api,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content');
var templates = {};
var $messages;
var $el = jQuery('#content');
var templates = {};
var $messages;
function init(params, loaded) {
topNavigationPresenter.select('register');
topNavigationPresenter.changeTitle('Registration');
promise.wait(util.promiseTemplate('registration-form'))
.then(function(template) {
templates.registration = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function init(params, loaded) {
topNavigationPresenter.select('register');
topNavigationPresenter.changeTitle('Registration');
promise.wait(util.promiseTemplate('registration-form'))
.then(function(template) {
templates.registration = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$el.html(templates.registration());
$el.find('form').submit(registrationFormSubmitted);
$messages = $el.find('.messages');
$messages.width($el.find('form').width());
}
function render() {
$el.html(templates.registration());
$el.find('form').submit(registrationFormSubmitted);
$messages = $el.find('.messages');
$messages.width($el.find('form').width());
}
function registrationFormSubmitted(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
function registrationFormSubmitted(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
var formData = {
userName: $el.find('[name=userName]').val(),
password: $el.find('[name=password]').val(),
passwordConfirmation: $el.find('[name=passwordConfirmation]').val(),
email: $el.find('[name=email]').val(),
};
var formData = {
userName: $el.find('[name=userName]').val(),
password: $el.find('[name=password]').val(),
passwordConfirmation: $el.find('[name=passwordConfirmation]').val(),
email: $el.find('[name=email]').val(),
};
if (!validateRegistrationFormData(formData)) {
return;
}
if (!validateRegistrationFormData(formData)) {
return;
}
promise.wait(api.post('/users', formData))
.then(function(response) {
registrationSuccess(response);
}).fail(function(response) {
registrationFailure(response);
});
}
promise.wait(api.post('/users', formData))
.then(function(response) {
registrationSuccess(response);
}).fail(function(response) {
registrationFailure(response);
});
}
function registrationSuccess(apiResponse) {
$el.find('form').slideUp(function() {
var message = 'Registration complete! ';
if (!apiResponse.json.confirmed) {
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
} else {
message += '<a href="#/login">Click here</a> to login.';
}
messagePresenter.showInfo($messages, message);
});
}
function registrationSuccess(apiResponse) {
$el.find('form').slideUp(function() {
var message = 'Registration complete! ';
if (!apiResponse.json.user.confirmed) {
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
} else {
message += '<a href="#/login">Click here</a> to login.';
}
messagePresenter.showInfo($messages, message);
});
}
function registrationFailure(apiResponse) {
messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse);
}
function registrationFailure(apiResponse) {
messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse);
}
function validateRegistrationFormData(formData) {
if (formData.userName.length === 0) {
messagePresenter.showError($messages, 'User name cannot be empty.');
return false;
}
function validateRegistrationFormData(formData) {
if (formData.userName.length === 0) {
messagePresenter.showError($messages, 'User name cannot be empty.');
return false;
}
if (formData.password.length === 0) {
messagePresenter.showError($messages, 'Password cannot be empty.');
return false;
}
if (formData.password.length === 0) {
messagePresenter.showError($messages, 'Password cannot be empty.');
return false;
}
if (formData.password !== formData.passwordConfirmation) {
messagePresenter.showError($messages, 'Passwords must be the same.');
return false;
}
if (formData.password !== formData.passwordConfirmation) {
messagePresenter.showError($messages, 'Passwords must be the same.');
return false;
}
return true;
}
return true;
}
return {
init: init,
render: render,
};
return {
init: init,
render: render,
};
};

View File

@ -2,133 +2,124 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.TagListPresenter = function(
_,
jQuery,
util,
promise,
keyboard,
pagerPresenter,
topNavigationPresenter) {
_,
jQuery,
util,
promise,
keyboard,
pagerPresenter,
topNavigationPresenter) {
var KEY_RETURN = 13;
var $el = jQuery('#content');
var $searchInput;
var templates = {};
var $el = jQuery('#content');
var $searchInput;
var templates = {};
var params;
var params;
function init(_params, loaded) {
topNavigationPresenter.select('tags');
topNavigationPresenter.changeTitle('Tags');
params = _params;
params.query = params.query || {};
function init(_params, loaded) {
topNavigationPresenter.select('tags');
topNavigationPresenter.changeTitle('Tags');
params = _params;
params.query = params.query || {};
promise.wait(
util.promiseTemplate('tag-list'),
util.promiseTemplate('tag-list-item'))
.then(function(listTemplate, listItemTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
promise.wait(
util.promiseTemplate('tag-list'),
util.promiseTemplate('tag-list-item'))
.then(function(listTemplate, listItemTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
render();
loaded();
render();
loaded();
pagerPresenter.init({
baseUri: '#/tags',
backendUri: '/tags',
$target: $el.find('.pagination-target'),
updateCallback: function($page, response) {
renderTags($page, response.json.tags);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
pagerPresenter.init({
baseUri: '#/tags',
backendUri: '/tags',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderTags($page, data.entities);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
params.query.order = params.query.order || 'name,asc';
updateActiveOrder(params.query.order);
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
params.query.order = params.query.order || 'name,asc';
updateActiveOrder(params.query.order);
pagerPresenter.reinit({query: params.query});
pagerPresenter.reinit({query: params.query});
keyboard.keyup('p', function() {
$el.find('table a').eq(0).focus();
});
keyboard.keyup('p', function() {
$el.find('table a').eq(0).focus();
});
keyboard.keyup('q', function() {
$searchInput.eq(0).focus().select();
});
keyboard.keyup('q', function() {
$searchInput.eq(0).focus().select();
});
loaded();
softRender();
}
loaded();
softRender();
}
function deinit() {
pagerPresenter.deinit();
}
function deinit() {
pagerPresenter.deinit();
}
function render() {
$el.html(templates.list());
$searchInput = $el.find('input[name=query]');
$el.find('form').submit(searchFormSubmitted);
App.Controls.AutoCompleteInput($searchInput);
softRender();
}
function render() {
$el.html(templates.list());
$searchInput = $el.find('input[name=query]');
$searchInput.keydown(searchInputKeyPressed);
$el.find('form').submit(searchFormSubmitted);
softRender();
}
function softRender() {
$searchInput.val(params.query.query);
}
function softRender() {
$searchInput.val(params.query.query);
}
function searchInputKeyPressed(e) {
if (e.which !== KEY_RETURN) {
return;
}
updateSearch();
}
function searchFormSubmitted(e) {
e.preventDefault();
updateSearch();
}
function searchFormSubmitted(e) {
e.preventDefault();
updateSearch();
}
function updateSearch() {
$searchInput.blur();
params.query.query = $searchInput.val().trim();
params.query.page = 1;
pagerPresenter.setQuery(params.query);
}
function updateSearch() {
$searchInput.blur();
params.query.query = $searchInput.val().trim();
params.query.page = 1;
pagerPresenter.setQuery(params.query);
}
function updateActiveOrder(activeOrder) {
$el.find('.order li a.active').removeClass('active');
$el.find('.order [href*="' + activeOrder + '"]').addClass('active');
}
function updateActiveOrder(activeOrder) {
$el.find('.order li a.active').removeClass('active');
$el.find('.order [href*="' + activeOrder + '"]').addClass('active');
}
function renderTags($page, tags) {
var $target = $page.find('tbody');
_.each(tags, function(tag) {
var $item = jQuery(templates.listItem({
tag: tag,
util: util,
}));
$target.append($item);
});
}
function renderTags($page, tags) {
var $target = $page.find('tbody');
_.each(tags, function(tag) {
var $item = jQuery(templates.listItem({
tag: tag,
formatRelativeTime: util.formatRelativeTime,
}));
$target.append($item);
});
}
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
};

View File

@ -2,182 +2,208 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.TagPresenter = function(
_,
jQuery,
util,
promise,
auth,
api,
tagList,
router,
keyboard,
topNavigationPresenter,
messagePresenter) {
_,
jQuery,
util,
promise,
auth,
api,
tagList,
router,
keyboard,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content');
var $messages = $el;
var templates = {};
var implicationsTagInput;
var suggestionsTagInput;
var $el = jQuery('#content');
var $messages = $el;
var templates = {};
var implicationsTagInput;
var suggestionsTagInput;
var tag;
var posts;
var siblings;
var tag;
var posts;
var siblings;
var privileges = {};
var privileges = {};
function init(params, loaded) {
topNavigationPresenter.select('tags');
topNavigationPresenter.changeTitle('Tags');
function init(params, loaded) {
topNavigationPresenter.select('tags');
topNavigationPresenter.changeTitle('Tags');
privileges.canChangeName = auth.hasPrivilege(auth.privileges.changeTagName);
privileges.canChangeCategory = auth.hasPrivilege(auth.privileges.changeTagCategory);
privileges.canChangeImplications = auth.hasPrivilege(auth.privileges.changeTagImplications);
privileges.canChangeSuggestions = auth.hasPrivilege(auth.privileges.changeTagSuggestions);
privileges.canBan = auth.hasPrivilege(auth.privileges.banTags);
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
privileges.canDelete = auth.hasPrivilege(auth.privileges.deleteTags);
privileges.canMerge = auth.hasPrivilege(auth.privileges.mergeTags);
privileges.canChangeName = auth.hasPrivilege(auth.privileges.changeTagName);
privileges.canChangeCategory = auth.hasPrivilege(auth.privileges.changeTagCategory);
privileges.canChangeImplications = auth.hasPrivilege(auth.privileges.changeTagImplications);
privileges.canChangeSuggestions = auth.hasPrivilege(auth.privileges.changeTagSuggestions);
privileges.canBan = auth.hasPrivilege(auth.privileges.banTags);
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
privileges.canDelete = auth.hasPrivilege(auth.privileges.deleteTags);
privileges.canMerge = auth.hasPrivilege(auth.privileges.mergeTags);
privileges.canViewPosts = auth.hasPrivilege(auth.privileges.viewPosts);
promise.wait(
util.promiseTemplate('tag'),
util.promiseTemplate('post-list-item'),
util.promiseTemplate('history'))
.then(function(tagTemplate, postListItemTemplate, historyTemplate) {
templates.tag = tagTemplate;
templates.postListItem = postListItemTemplate;
templates.history = historyTemplate;
promise.wait(
util.promiseTemplate('tag'),
util.promiseTemplate('post-list-item'),
util.promiseTemplate('history'))
.then(function(tagTemplate, postListItemTemplate, historyTemplate) {
templates.tag = tagTemplate;
templates.postListItem = postListItemTemplate;
templates.history = historyTemplate;
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
}
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(params, loaded) {
var tagName = params.tagName;
function reinit(params, loaded) {
var tagName = params.tagName;
messagePresenter.hideMessages($messages);
messagePresenter.hideMessages($messages);
promise.wait(
api.get('tags/' + tagName),
api.get('tags/' + tagName + '/siblings'),
api.get('posts', {query: tagName}))
.then(function(tagResponse, siblingsResponse, postsResponse) {
tag = tagResponse.json;
siblings = siblingsResponse.json.data;
posts = postsResponse.json.data;
posts = posts.slice(0, 8);
promise.wait(
api.get('tags/' + tagName),
api.get('tags/' + tagName + '/siblings'),
api.get('posts', {query: tagName}))
.then(function(tagResponse, siblingsResponse, postsResponse) {
tag = tagResponse.json.tag;
siblings = siblingsResponse.json.tags;
posts = postsResponse.json.posts;
posts = posts.slice(0, 8);
render();
loaded();
render();
loaded();
renderPosts(posts);
}).fail(function(tagResponse, siblingsResponse, postsResponse) {
messagePresenter.showError($messages, tagResponse.json.error || siblingsResponse.json.error || postsResponse.json.error);
loaded();
});
}
renderPosts(posts);
}).fail(function(tagResponse, siblingsResponse, postsResponse) {
messagePresenter.showError($messages, tagResponse.json.error || siblingsResponse.json.error || postsResponse.json.error);
loaded();
});
}
function render() {
$el.html(templates.tag({
privileges: privileges,
tag: tag,
siblings: siblings,
tagCategories: JSON.parse(jQuery('head').attr('data-tag-categories')),
formatRelativeTime: util.formatRelativeTime,
historyTemplate: templates.history,
}));
$el.find('.post-list').hide();
$el.find('form').submit(function(e) { e.preventDefault(); });
$el.find('form button[name=update]').click(updateButtonClicked);
$el.find('form button[name=delete]').click(deleteButtonClicked);
$el.find('form button[name=merge]').click(mergeButtonClicked);
implicationsTagInput = App.Controls.TagInput($el.find('[name=implications]'));
suggestionsTagInput = App.Controls.TagInput($el.find('[name=suggestions]'));
}
function getTagCategories() {
var tagCategories = JSON.parse(jQuery('head').attr('data-tag-categories'));
var result = {};
jQuery.each(tagCategories, function(i, item) {
result[item[0]] = item[1];
});
return result;
}
function updateButtonClicked(e) {
e.preventDefault();
var $form = $el.find('form');
var formData = {};
function render() {
$el.html(templates.tag({
privileges: privileges,
tag: tag,
siblings: siblings,
tagCategories: getTagCategories(),
util: util,
historyTemplate: templates.history,
}));
$el.find('.post-list').hide();
$el.find('form').submit(function(e) { e.preventDefault(); });
$el.find('form button[name=update]').click(updateButtonClicked);
$el.find('form button[name=delete]').click(deleteButtonClicked);
$el.find('form button[name=merge]').click(mergeButtonClicked);
implicationsTagInput = App.Controls.TagInput($el.find('[name=implications]'));
suggestionsTagInput = App.Controls.TagInput($el.find('[name=suggestions]'));
}
if (privileges.canChangeName) {
formData.name = $form.find('[name=name]').val();
}
function updateButtonClicked(e) {
e.preventDefault();
var $form = $el.find('form');
var formData = {};
if (privileges.canChangeCategory) {
formData.category = $form.find('[name=category]:checked').val();
}
if (privileges.canChangeName) {
formData.name = $form.find('[name=name]').val();
}
if (privileges.canBan) {
formData.banned = $form.find('[name=ban]').is(':checked') ? 1 : 0;
}
if (privileges.canChangeCategory) {
formData.category = $form.find('[name=category]:checked').val();
}
if (privileges.canChangeImplications) {
formData.implications = implicationsTagInput.getTags().join(' ');
}
if (privileges.canBan) {
formData.banned = $form.find('[name=ban]').is(':checked') ? 1 : 0;
}
if (privileges.canChangeSuggestions) {
formData.suggestions = suggestionsTagInput.getTags().join(' ');
}
if (privileges.canChangeImplications) {
formData.implications = implicationsTagInput.getTags().join(' ');
}
promise.wait(api.put('/tags/' + tag.name, formData))
.then(function(response) {
router.navigateInplace('#/tag/' + response.json.name);
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
}
if (privileges.canChangeSuggestions) {
formData.suggestions = suggestionsTagInput.getTags().join(' ');
}
function deleteButtonClicked(e) {
if (!window.confirm('Are you sure you want to delete this tag?')) {
return;
}
promise.wait(api.delete('/tags/' + tag.name))
.then(function(response) {
router.navigate('#/tags');
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
}
promise.wait(api.put('/tags/' + tag.name, formData))
.then(function(response) {
router.navigateInplace('#/tag/' + response.json.tag.name);
tagList.refreshTags();
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
}
function mergeButtonClicked(e) {
var targetTag = window.prompt('What tag should this be merged to?');
if (targetTag) {
promise.wait(api.put('/tags/' + tag.name + '/merge', {targetTag: targetTag}))
.then(function(response) {
router.navigate('#/tags');
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
}
}
function deleteButtonClicked(e) {
if (!window.confirm('Are you sure you want to delete this tag?')) {
return;
}
promise.wait(api.delete('/tags/' + tag.name))
.then(function(response) {
router.navigate('#/tags');
tagList.refreshTags();
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
}
function renderPosts(posts) {
var $target = $el.find('.post-list ul');
_.each(posts, function(post) {
var $post = jQuery('<li>' + templates.postListItem({
util: util,
post: post,
query: {query: tag.name},
}) + '</li>');
$target.append($post);
});
if (posts.length > 0) {
$el.find('.post-list').fadeIn();
keyboard.keyup('p', function() {
$el.find('.post-list a').eq(0).focus();
});
}
}
function mergeButtonClicked(e) {
var targetTag = window.prompt('What tag should this be merged to?');
if (targetTag) {
promise.wait(api.put('/tags/' + tag.name + '/merge', {targetTag: targetTag}))
.then(function(response) {
router.navigate('#/tags');
tagList.refreshTags();
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
}
}
return {
init: init,
reinit: reinit,
};
function renderPosts(posts) {
var $target = $el.find('.post-list ul');
_.each(posts, function(post) {
var $post = jQuery('<li>' + templates.postListItem({
util: util,
post: post,
query: {query: tag.name},
canViewPosts: privileges.canViewPosts,
}) + '</li>');
$target.append($post);
});
if (posts.length > 0) {
$el.find('.post-list').fadeIn();
keyboard.keyup('p', function() {
$el.find('.post-list a').eq(0).focus();
});
}
}
return {
init: init,
reinit: reinit,
};
};
App.DI.register('tagPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'api', 'tagList', 'router', 'keyboard', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.TagPresenter);
App.DI.register('tagPresenter', [
'_',
'jQuery',
'util',
'promise',
'auth',
'api',
'tagList',
'router',
'keyboard',
'topNavigationPresenter',
'messagePresenter'],
App.Presenters.TagPresenter);

View File

@ -2,78 +2,78 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.TopNavigationPresenter = function(
jQuery,
util,
promise,
auth) {
jQuery,
util,
promise,
auth) {
var selectedElement = null;
var $el = jQuery('#top-navigation');
var templates = {};
var baseTitle = document.title;
var selectedElement = null;
var $el = jQuery('#top-navigation');
var templates = {};
var baseTitle = document.title;
function init(params, loaded) {
promise.wait(util.promiseTemplate('top-navigation'))
.then(function(template) {
templates.topNavigation = template;
render();
loaded();
auth.startObservingLoginChanges('top-navigation', loginStateChanged);
}).fail(function() {
loaded();
});
}
function init(params, loaded) {
promise.wait(util.promiseTemplate('top-navigation'))
.then(function(template) {
templates.topNavigation = template;
render();
loaded();
auth.startObservingLoginChanges('top-navigation', loginStateChanged);
}).fail(function() {
loaded();
});
}
function select(newSelectedElement) {
selectedElement = newSelectedElement;
$el.find('li a').removeClass('active');
$el.find('li.' + selectedElement).find('a').addClass('active');
}
function select(newSelectedElement) {
selectedElement = newSelectedElement;
$el.find('li a').removeClass('active');
$el.find('li.' + selectedElement).find('a').addClass('active');
}
function loginStateChanged() {
render();
}
function loginStateChanged() {
render();
}
function render() {
$el.html(templates.topNavigation({
loggedIn: auth.isLoggedIn(),
user: auth.getCurrentUser(),
canListUsers: auth.hasPrivilege(auth.privileges.listUsers),
canListPosts: auth.hasPrivilege(auth.privileges.listPosts),
canListComments: auth.hasPrivilege(auth.privileges.listComments),
canListTags: auth.hasPrivilege(auth.privileges.listTags),
canUploadPosts: auth.hasPrivilege(auth.privileges.uploadPosts),
}));
$el.find('li.' + selectedElement).find('a').addClass('active');
}
function render() {
$el.html(templates.topNavigation({
loggedIn: auth.isLoggedIn(),
user: auth.getCurrentUser(),
canListUsers: auth.hasPrivilege(auth.privileges.listUsers),
canListPosts: auth.hasPrivilege(auth.privileges.listPosts),
canListComments: auth.hasPrivilege(auth.privileges.listComments),
canListTags: auth.hasPrivilege(auth.privileges.listTags),
canUploadPosts: auth.hasPrivilege(auth.privileges.uploadPosts),
}));
$el.find('li.' + selectedElement).find('a').addClass('active');
}
function focus() {
var $tmp = jQuery('<a href="#"> </a>');
$el.prepend($tmp);
$tmp.focus();
$tmp.remove();
}
function focus() {
var $tmp = jQuery('<a href="#"> </a>');
$el.prepend($tmp);
$tmp.focus();
$tmp.remove();
}
function getBaseTitle() {
return baseTitle;
}
function getBaseTitle() {
return baseTitle;
}
function changeTitle(subTitle) {
var newTitle = baseTitle;
if (subTitle) {
newTitle += ' - ' + subTitle;
}
document.title = newTitle;
}
function changeTitle(subTitle) {
var newTitle = baseTitle;
if (subTitle) {
newTitle += ' - ' + subTitle;
}
document.title = newTitle;
}
return {
init: init,
render: render,
select: select,
focus: focus,
getBaseTitle: getBaseTitle,
changeTitle: changeTitle,
};
return {
init: init,
render: render,
select: select,
focus: focus,
getBaseTitle: getBaseTitle,
changeTitle: changeTitle,
};
};

View File

@ -2,80 +2,82 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.UserAccountRemovalPresenter = function(
jQuery,
util,
promise,
api,
auth,
router,
messagePresenter) {
jQuery,
util,
promise,
api,
auth,
router,
messagePresenter) {
var target;
var templates = {};
var user;
var privileges = {};
var target;
var templates = {};
var user;
var privileges = {};
function init(params, loaded) {
user = params.user;
target = params.target;
function init(params, loaded) {
user = params.user;
target = params.target;
privileges.canDeleteAccount =
auth.hasPrivilege(auth.privileges.deleteAllAccounts) ||
(auth.hasPrivilege(auth.privileges.deleteOwnAccount) && auth.isLoggedIn(user.name));
privileges.canDeleteAccount =
auth.hasPrivilege(auth.privileges.deleteAllAccounts) ||
(auth.hasPrivilege(auth.privileges.deleteOwnAccount) && auth.isLoggedIn(user.name));
promise.wait(util.promiseTemplate('account-removal'))
.then(function(template) {
templates.accountRemoval = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('account-removal'))
.then(function(template) {
templates.accountRemoval = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
var $el = jQuery(target);
$el.html(templates.accountRemoval({
user: user,
canDeleteAccount: privileges.canDeleteAccount}));
function render() {
var $el = jQuery(target);
$el.html(templates.accountRemoval({
user: user,
canDeleteAccount: privileges.canDeleteAccount}));
$el.find('form').submit(accountRemovalFormSubmitted);
}
$el.find('form').submit(accountRemovalFormSubmitted);
}
function getPrivileges() {
return privileges;
}
function getPrivileges() {
return privileges;
}
function accountRemovalFormSubmitted(e) {
e.preventDefault();
var $el = jQuery(target);
var $messages = $el.find('.messages');
messagePresenter.hideMessages($messages);
if (!$el.find('input[name=confirmation]:visible').prop('checked')) {
messagePresenter.showError($messages, 'Must confirm to proceed.');
return;
}
promise.wait(api.delete('/users/' + user.name))
.then(function() {
auth.logout();
var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. <a href="">Back to main page</a>');
$messageDiv.find('a').click(mainPageLinkClicked);
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
function accountRemovalFormSubmitted(e) {
e.preventDefault();
var $el = jQuery(target);
var $messages = $el.find('.messages');
messagePresenter.hideMessages($messages);
if (!$el.find('input[name=confirmation]:visible').prop('checked')) {
messagePresenter.showError($messages, 'Must confirm to proceed.');
return;
}
promise.wait(api.delete('/users/' + user.name))
.then(function() {
if (user.name === auth.getCurrentUser().name) {
auth.logout();
}
var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. <a href="">Back to main page</a>');
$messageDiv.find('a').click(mainPageLinkClicked);
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
function mainPageLinkClicked(e) {
e.preventDefault();
router.navigateToMainPage();
}
function mainPageLinkClicked(e) {
e.preventDefault();
router.navigateToMainPage();
}
return {
init: init,
render: render,
getPrivileges: getPrivileges
};
return {
init: init,
render: render,
getPrivileges: getPrivileges
};
};

View File

@ -2,172 +2,162 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.UserAccountSettingsPresenter = function(
_,
jQuery,
util,
promise,
api,
auth,
messagePresenter) {
_,
jQuery,
util,
promise,
api,
auth,
messagePresenter) {
var target;
var templates = {};
var user;
var privileges;
var avatarContent;
var fileDropper;
var target;
var templates = {};
var user;
var privileges;
var avatarContent;
var fileDropper;
function init(params, loaded) {
user = params.user;
target = params.target;
function init(params, loaded) {
user = params.user;
target = params.target;
privileges = {
canBan:
auth.hasPrivilege(auth.privileges.banUsers),
canChangeAccessRank:
auth.hasPrivilege(auth.privileges.changeAccessRank),
canChangeAvatarStyle:
auth.hasPrivilege(auth.privileges.changeAllAvatarStyles) ||
(auth.hasPrivilege(auth.privileges.changeOwnAvatarStyle) && auth.isLoggedIn(user.name)),
canChangeName:
auth.hasPrivilege(auth.privileges.changeAllNames) ||
(auth.hasPrivilege(auth.privileges.changeOwnName) && auth.isLoggedIn(user.name)),
canChangeEmailAddress:
auth.hasPrivilege(auth.privileges.changeAllEmailAddresses) ||
(auth.hasPrivilege(auth.privileges.changeOwnEmailAddress) && auth.isLoggedIn(user.name)),
canChangePassword:
auth.hasPrivilege(auth.privileges.changeAllPasswords) ||
(auth.hasPrivilege(auth.privileges.changeOwnPassword) && auth.isLoggedIn(user.name)),
};
privileges = {
canBan:
auth.hasPrivilege(auth.privileges.banUsers),
canChangeAccessRank:
auth.hasPrivilege(auth.privileges.changeAccessRank),
canChangeAvatarStyle:
auth.hasPrivilege(auth.privileges.changeAllAvatarStyles) ||
(auth.hasPrivilege(auth.privileges.changeOwnAvatarStyle) && auth.isLoggedIn(user.name)),
canChangeName:
auth.hasPrivilege(auth.privileges.changeAllNames) ||
(auth.hasPrivilege(auth.privileges.changeOwnName) && auth.isLoggedIn(user.name)),
canChangeEmailAddress:
auth.hasPrivilege(auth.privileges.changeAllEmailAddresses) ||
(auth.hasPrivilege(auth.privileges.changeOwnEmailAddress) && auth.isLoggedIn(user.name)),
canChangePassword:
auth.hasPrivilege(auth.privileges.changeAllPasswords) ||
(auth.hasPrivilege(auth.privileges.changeOwnPassword) && auth.isLoggedIn(user.name)),
};
promise.wait(util.promiseTemplate('account-settings'))
.then(function(template) {
templates.accountRemoval = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('account-settings'))
.then(function(template) {
templates.accountRemoval = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
var $el = jQuery(target);
$el.html(templates.accountRemoval(_.extend({user: user}, privileges)));
$el.find('form').submit(accountSettingsFormSubmitted);
$el.find('form [name=avatar-style]').change(avatarStyleChanged);
avatarStyleChanged();
fileDropper = new App.Controls.FileDropper($el.find('[name=avatar-content]'));
fileDropper.onChange = avatarContentChanged;
fileDropper.setNames = true;
}
function render() {
var $el = jQuery(target);
$el.html(templates.accountRemoval(_.extend({user: user}, privileges)));
$el.find('form').submit(accountSettingsFormSubmitted);
$el.find('form [name=avatar-style]').change(avatarStyleChanged);
avatarStyleChanged();
fileDropper = new App.Controls.FileDropper($el.find('[name=avatar-content]'));
fileDropper.onChange = avatarContentChanged;
fileDropper.setNames = true;
}
function getPrivileges() {
return privileges;
}
function getPrivileges() {
return privileges;
}
function avatarStyleChanged(e) {
var $el = jQuery(target);
var $target = $el.find('.avatar-content .file-handler');
if ($el.find('[name=avatar-style]:checked').val() === 'manual') {
$target.show();
} else {
$target.hide();
}
}
function avatarStyleChanged(e) {
var $el = jQuery(target);
var $target = $el.find('.avatar-content .file-handler');
if ($el.find('[name=avatar-style]:checked').val() === 'manual') {
$target.show();
} else {
$target.hide();
}
}
function avatarContentChanged(files) {
if (files.length === 1) {
fileDropper.readAsDataURL(files[0], function(content) {
avatarContent = content;
});
}
}
function avatarContentChanged(files) {
avatarContent = files[0];
}
function accountSettingsFormSubmitted(e) {
e.preventDefault();
var $el = jQuery(target);
var $messages = jQuery(target).find('.messages');
messagePresenter.hideMessages($messages);
var formData = {};
function accountSettingsFormSubmitted(e) {
e.preventDefault();
var $el = jQuery(target);
var $messages = jQuery(target).find('.messages');
messagePresenter.hideMessages($messages);
var formData = new FormData();
if (privileges.canChangeAvatarStyle) {
formData.avatarStyle = $el.find('[name=avatar-style]:checked').val();
if (avatarContent) {
formData.avatarContent = avatarContent;
}
}
if (privileges.canChangeName) {
formData.userName = $el.find('[name=userName]').val();
}
if (privileges.canChangeEmailAddress) {
formData.email = $el.find('[name=email]').val();
}
if (privileges.canChangePassword) {
formData.password = $el.find('[name=password]').val();
formData.passwordConfirmation = $el.find('[name=passwordConfirmation]').val();
}
if (privileges.canChangeAccessRank) {
formData.accessRank = $el.find('[name=access-rank]:checked').val();
}
if (privileges.canBan) {
formData.banned = $el.find('[name=ban]').is(':checked') ? 1 : 0;
}
if (privileges.canChangeAvatarStyle) {
formData.append('avatarStyle', $el.find('[name=avatar-style]:checked').val());
if (avatarContent) {
formData.append('avatarContent', avatarContent);
}
}
if (privileges.canChangeName) {
formData.append('userName', $el.find('[name=userName]').val());
}
if (!validateAccountSettingsFormData(formData)) {
return;
}
if (privileges.canChangeEmailAddress) {
formData.append('email', $el.find('[name=email]').val());
}
if (!formData.password) {
delete formData.password;
delete formData.passwordConfirmation;
}
if (privileges.canChangePassword) {
var password = $el.find('[name=password]').val();
var passwordConfirmation = $el.find('[name=passwordConfirmation]').val();
promise.wait(api.put('/users/' + user.name, formData))
.then(function(response) {
editSuccess(response);
}).fail(function(response) {
editFailure(response);
});
}
if (password) {
if (password !== passwordConfirmation) {
messagePresenter.showError($messages, 'Passwords must be the same.');
return;
}
function editSuccess(apiResponse) {
var wasLoggedIn = auth.isLoggedIn(user.name);
user = apiResponse.json;
if (wasLoggedIn) {
auth.updateCurrentUser(user);
}
formData.append('password', password);
}
}
render();
if (privileges.canChangeAccessRank) {
formData.append('accessRank', $el.find('[name=access-rank]:checked').val());
}
var $messages = jQuery(target).find('.messages');
var message = 'Account settings updated!';
if (!apiResponse.json.confirmed) {
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
}
messagePresenter.showInfo($messages, message);
}
if (privileges.canBan) {
formData.append('banned', $el.find('[name=ban]').is(':checked') ? 1 : 0);
}
function editFailure(apiResponse) {
var $messages = jQuery(target).find('.messages');
messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse);
}
promise.wait(api.post('/users/' + user.name, formData))
.then(function(response) {
editSuccess(response);
}).fail(function(response) {
editFailure(response);
});
}
function validateAccountSettingsFormData(formData) {
var $messages = jQuery(target).find('.messages');
if (formData.password !== formData.passwordConfirmation) {
messagePresenter.showError($messages, 'Passwords must be the same.');
return false;
}
function editSuccess(apiResponse) {
var wasLoggedIn = auth.isLoggedIn(user.name);
user = apiResponse.json.user;
if (wasLoggedIn) {
auth.updateCurrentUser(user);
}
return true;
}
render();
return {
init: init,
render: render,
getPrivileges: getPrivileges,
};
var $messages = jQuery(target).find('.messages');
var message = 'Account settings updated!';
if (!apiResponse.json.user.confirmed) {
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
}
messagePresenter.showInfo($messages, message);
}
function editFailure(apiResponse) {
var $messages = jQuery(target).find('.messages');
messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse);
}
return {
init: init,
render: render,
getPrivileges: getPrivileges,
};
};

View File

@ -2,117 +2,117 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.UserActivationPresenter = function(
jQuery,
promise,
util,
auth,
api,
router,
topNavigationPresenter,
messagePresenter) {
jQuery,
promise,
util,
auth,
api,
router,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content');
var $messages = $el;
var templates = {};
var formHidden = false;
var operation;
var $el = jQuery('#content');
var $messages = $el;
var templates = {};
var formHidden = false;
var operation;
function init(params, loaded) {
topNavigationPresenter.select('login');
topNavigationPresenter.changeTitle('Account recovery');
reinit(params, loaded);
}
function init(params, loaded) {
topNavigationPresenter.select('login');
topNavigationPresenter.changeTitle('Account recovery');
reinit(params, loaded);
}
function reinit(params, loaded) {
operation = params.operation;
promise.wait(util.promiseTemplate('user-query-form'))
.then(function(template) {
templates.userQuery = template;
if (params.token) {
hideForm();
confirmToken(params.token);
} else {
showForm();
}
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(params, loaded) {
operation = params.operation;
promise.wait(util.promiseTemplate('user-query-form'))
.then(function(template) {
templates.userQuery = template;
if (params.token) {
hideForm();
confirmToken(params.token);
} else {
showForm();
}
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$el.html(templates.userQuery());
$messages = $el.find('.messages');
if (formHidden) {
$el.find('form').hide();
}
$el.find('form').submit(userQueryFormSubmitted);
}
function render() {
$el.html(templates.userQuery());
$messages = $el.find('.messages');
if (formHidden) {
$el.find('form').hide();
}
$el.find('form').submit(userQueryFormSubmitted);
}
function hideForm() {
formHidden = true;
}
function hideForm() {
formHidden = true;
}
function showForm() {
formHidden = false;
}
function showForm() {
formHidden = false;
}
function userQueryFormSubmitted(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
function userQueryFormSubmitted(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
var userNameOrEmail = $el.find('form input[name=user]').val();
if (userNameOrEmail.length === 0) {
messagePresenter.showError($messages, 'Field cannot be blank.');
return;
}
var url = operation === 'passwordReset' ?
'/password-reset/' + userNameOrEmail :
'/activation/' + userNameOrEmail;
var userNameOrEmail = $el.find('form input[name=user]').val();
if (userNameOrEmail.length === 0) {
messagePresenter.showError($messages, 'Field cannot be blank.');
return;
}
var url = operation === 'passwordReset' ?
'/password-reset/' + userNameOrEmail :
'/activation/' + userNameOrEmail;
promise.wait(api.post(url))
.then(function(response) {
var message = operation === 'passwordReset' ?
'Password reset request sent.' :
'Activation e-mail resent.';
message += ' Check your inbox.<br/>If e-mail doesn\'t show up, check your spam folder.';
promise.wait(api.post(url))
.then(function(response) {
var message = operation === 'passwordReset' ?
'Password reset request sent.' :
'Activation e-mail resent.';
message += ' Check your inbox.<br/>If e-mail doesn\'t show up, check your spam folder.';
$el.find('#user-query-form').slideUp(function() {
messagePresenter.showInfo($messages, message);
});
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
$el.find('#user-query-form').slideUp(function() {
messagePresenter.showInfo($messages, message);
});
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
function confirmToken(token) {
messagePresenter.hideMessages($messages);
function confirmToken(token) {
messagePresenter.hideMessages($messages);
var url = operation === 'passwordReset' ?
'/finish-password-reset/' + token :
'/finish-activation/' + token;
var url = operation === 'passwordReset' ?
'/finish-password-reset/' + token :
'/finish-activation/' + token;
promise.wait(api.post(url))
.then(function(response) {
var message = operation === 'passwordReset' ?
'Your new password is <strong>' + response.json.newPassword + '</strong>.' :
'E-mail activation successful.';
promise.wait(api.post(url))
.then(function(response) {
var message = operation === 'passwordReset' ?
'Your new password is <strong>' + response.json.newPassword + '</strong>.' :
'E-mail activation successful.';
$el.find('#user-query-form').slideUp(function() {
messagePresenter.showInfo($messages, message);
});
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
$el.find('#user-query-form').slideUp(function() {
messagePresenter.showInfo($messages, message);
});
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
return {
init: init,
reinit: reinit,
render: render,
};
return {
init: init,
reinit: reinit,
render: render,
};
};

View File

@ -2,74 +2,77 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.UserBrowsingSettingsPresenter = function(
jQuery,
util,
promise,
auth,
browsingSettings,
messagePresenter) {
jQuery,
util,
promise,
auth,
browsingSettings,
messagePresenter) {
var target;
var templates = {};
var user;
var privileges = {};
var target;
var templates = {};
var user;
var privileges = {};
function init(params, loaded) {
user = params.user;
target = params.target;
function init(params, loaded) {
user = params.user;
target = params.target;
privileges.canChangeBrowsingSettings = auth.isLoggedIn(user.name) && user.name === auth.getCurrentUser().name;
privileges.canChangeBrowsingSettings = auth.isLoggedIn(user.name) && user.name === auth.getCurrentUser().name;
promise.wait(util.promiseTemplate('browsing-settings'))
.then(function(template) {
templates.browsingSettings = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
promise.wait(util.promiseTemplate('browsing-settings'))
.then(function(template) {
templates.browsingSettings = template;
render();
loaded();
}).fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
var $el = jQuery(target);
$el.html(templates.browsingSettings({user: user, settings: browsingSettings.getSettings()}));
$el.find('form').submit(browsingSettingsFormSubmitted);
}
function render() {
var $el = jQuery(target);
$el.html(templates.browsingSettings({user: user, settings: browsingSettings.getSettings()}));
$el.find('form').submit(browsingSettingsFormSubmitted);
}
function browsingSettingsFormSubmitted(e) {
e.preventDefault();
var $el = jQuery(target);
var $messages = $el.find('.messages');
messagePresenter.hideMessages($messages);
function browsingSettingsFormSubmitted(e) {
e.preventDefault();
var $el = jQuery(target);
var $messages = $el.find('.messages');
messagePresenter.hideMessages($messages);
var newSettings = {
endlessScroll: $el.find('[name=endlessScroll]').is(':checked'),
hideDownvoted: $el.find('[name=hideDownvoted]').is(':checked'),
listPosts: {
safe: $el.find('[name=listSafePosts]').is(':checked'),
sketchy: $el.find('[name=listSketchyPosts]').is(':checked'),
unsafe: $el.find('[name=listUnsafePosts]').is(':checked'),
},
};
var newSettings = {
endlessScroll: $el.find('[name=endlessScroll]').is(':checked'),
hideDownvoted: $el.find('[name=hideDownvoted]').is(':checked'),
listPosts: {
safe: $el.find('[name=listSafePosts]').is(':checked'),
sketchy: $el.find('[name=listSketchyPosts]').is(':checked'),
unsafe: $el.find('[name=listUnsafePosts]').is(':checked'),
},
keyboardShortcuts: $el.find('[name=keyboardShortcuts]').is(':checked'),
fitMode: $el.find('[name=fitMode]:checked').val(),
upscale: $el.find('[name=upscale]').is(':checked'),
};
promise.wait(browsingSettings.setSettings(newSettings))
.then(function() {
messagePresenter.showInfo($messages, 'Browsing settings updated!');
}).fail(function() {
console.log(arguments);
});
}
promise.wait(browsingSettings.setSettings(newSettings))
.then(function() {
messagePresenter.showInfo($messages, 'Browsing settings updated!');
}).fail(function() {
console.log(arguments);
});
}
function getPrivileges() {
return privileges;
}
function getPrivileges() {
return privileges;
}
return {
init: init,
render: render,
getPrivileges: getPrivileges,
};
return {
init: init,
render: render,
getPrivileges: getPrivileges,
};
};

View File

@ -2,90 +2,93 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.UserListPresenter = function(
_,
jQuery,
util,
promise,
auth,
pagerPresenter,
topNavigationPresenter) {
_,
jQuery,
util,
promise,
auth,
pagerPresenter,
topNavigationPresenter) {
var $el = jQuery('#content');
var templates = {};
var params;
var $el = jQuery('#content');
var templates = {};
var params;
var privileges = {};
function init(params, loaded) {
topNavigationPresenter.select('users');
topNavigationPresenter.changeTitle('Users');
function init(params, loaded) {
topNavigationPresenter.select('users');
topNavigationPresenter.changeTitle('Users');
promise.wait(
util.promiseTemplate('user-list'),
util.promiseTemplate('user-list-item'))
.then(function(listTemplate, listItemTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
privileges.canViewUsers = auth.hasPrivilege(auth.privileges.viewUsers);
render();
loaded();
promise.wait(
util.promiseTemplate('user-list'),
util.promiseTemplate('user-list-item'))
.then(function(listTemplate, listItemTemplate) {
templates.list = listTemplate;
templates.listItem = listItemTemplate;
pagerPresenter.init({
baseUri: '#/users',
backendUri: '/users',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderUsers($page, data.entities);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
render();
loaded();
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
params.query.order = params.query.order || 'name,asc';
updateActiveOrder(params.query.order);
pagerPresenter.init({
baseUri: '#/users',
backendUri: '/users',
$target: $el.find('.pagination-target'),
updateCallback: function($page, response) {
renderUsers($page, response.json.users);
},
},
function() {
reinit(params, function() {});
});
}).fail(function() {
console.log(arguments);
loaded();
});
}
pagerPresenter.reinit({query: params.query});
loaded();
}
function reinit(_params, loaded) {
params = _params;
params.query = params.query || {};
params.query.order = params.query.order || 'name,asc';
updateActiveOrder(params.query.order);
function deinit() {
pagerPresenter.deinit();
}
pagerPresenter.reinit({query: params.query});
loaded();
}
function render() {
$el.html(templates.list());
}
function deinit() {
pagerPresenter.deinit();
}
function updateActiveOrder(activeOrder) {
$el.find('.order li a.active').removeClass('active');
$el.find('.order [href*="' + activeOrder + '"]').addClass('active');
}
function render() {
$el.html(templates.list(privileges));
}
function renderUsers($page, users) {
var $target = $page.find('.users');
_.each(users, function(user) {
var $item = jQuery('<li>' + templates.listItem({
user: user,
formatRelativeTime: util.formatRelativeTime,
}) + '</li>');
$target.append($item);
});
_.map(_.map($target.find('img'), jQuery), util.loadImagesNicely);
}
function updateActiveOrder(activeOrder) {
$el.find('.order li a.active').removeClass('active');
$el.find('.order [href*="' + activeOrder + '"]').addClass('active');
}
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
function renderUsers($page, users) {
var $target = $page.find('.users');
_.each(users, function(user) {
var $item = jQuery('<li>' + templates.listItem(_.extend({
user: user,
util: util,
}, privileges)) + '</li>');
$target.append($item);
});
_.map(_.map($target.find('img'), jQuery), util.loadImagesNicely);
}
return {
init: init,
reinit: reinit,
deinit: deinit,
render: render,
};
};

View File

@ -2,105 +2,105 @@ var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.UserPresenter = function(
_,
jQuery,
util,
promise,
api,
auth,
topNavigationPresenter,
presenterManager,
userBrowsingSettingsPresenter,
userAccountSettingsPresenter,
userAccountRemovalPresenter,
messagePresenter) {
_,
jQuery,
util,
promise,
api,
auth,
topNavigationPresenter,
presenterManager,
userBrowsingSettingsPresenter,
userAccountSettingsPresenter,
userAccountRemovalPresenter,
messagePresenter) {
var $el = jQuery('#content');
var $messages = $el;
var templates = {};
var user;
var userName = null;
var activeTab;
var $el = jQuery('#content');
var $messages = $el;
var templates = {};
var user;
var userName = null;
var activeTab;
function init(params, loaded) {
promise.wait(util.promiseTemplate('user'))
.then(function(template) {
templates.user = template;
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
}
function init(params, loaded) {
promise.wait(util.promiseTemplate('user'))
.then(function(template) {
templates.user = template;
reinit(params, loaded);
}).fail(function() {
console.log(arguments);
loaded();
});
}
function reinit(params, loaded) {
if (params.userName !== userName) {
userName = params.userName;
topNavigationPresenter.select(auth.isLoggedIn(userName) ? 'my-account' : 'users');
topNavigationPresenter.changeTitle(userName);
function reinit(params, loaded) {
if (params.userName !== userName) {
userName = params.userName;
topNavigationPresenter.select(auth.isLoggedIn(userName) ? 'my-account' : 'users');
topNavigationPresenter.changeTitle(userName);
promise.wait(api.get('/users/' + userName))
.then(function(response) {
user = response.json;
var extendedContext = _.extend(params, {user: user});
promise.wait(api.get('/users/' + userName))
.then(function(response) {
user = response.json.user;
var extendedContext = _.extend(params, {user: user});
presenterManager.initPresenters([
[userBrowsingSettingsPresenter, _.extend({}, extendedContext, {target: '#browsing-settings-target'})],
[userAccountSettingsPresenter, _.extend({}, extendedContext, {target: '#account-settings-target'})],
[userAccountRemovalPresenter, _.extend({}, extendedContext, {target: '#account-removal-target'})]],
function() {
initTabs(params);
loaded();
});
presenterManager.initPresenters([
[userBrowsingSettingsPresenter, _.extend({}, extendedContext, {target: '#browsing-settings-target'})],
[userAccountSettingsPresenter, _.extend({}, extendedContext, {target: '#account-settings-target'})],
[userAccountRemovalPresenter, _.extend({}, extendedContext, {target: '#account-removal-target'})]],
function() {
initTabs(params);
loaded();
});
}).fail(function(response) {
$el.empty();
messagePresenter.showError($messages, response.json && response.json.error || response);
loaded();
});
}).fail(function(response) {
$el.empty();
messagePresenter.showError($messages, response.json && response.json.error || response);
loaded();
});
} else {
initTabs(params);
loaded();
}
}
} else {
initTabs(params);
loaded();
}
}
function initTabs(params) {
activeTab = params.tab || 'basic-info';
render();
}
function initTabs(params) {
activeTab = params.tab || 'basic-info';
render();
}
function render() {
$el.html(templates.user({
user: user,
isLoggedIn: auth.isLoggedIn(user.name),
formatRelativeTime: util.formatRelativeTime,
canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings,
canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()),
canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount}));
$messages = $el.find('.messages');
util.loadImagesNicely($el.find('img'));
userBrowsingSettingsPresenter.render();
userAccountSettingsPresenter.render();
userAccountRemovalPresenter.render();
changeTab(activeTab);
}
function render() {
$el.html(templates.user({
user: user,
isLoggedIn: auth.isLoggedIn(user.name),
util: util,
canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings,
canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()),
canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount}));
$messages = $el.find('.messages');
util.loadImagesNicely($el.find('img'));
userBrowsingSettingsPresenter.render();
userAccountSettingsPresenter.render();
userAccountRemovalPresenter.render();
changeTab(activeTab);
}
function changeTab(targetTab) {
var $link = $el.find('a[data-tab=' + targetTab + ']');
var $links = $link.closest('ul').find('a[data-tab]');
var $tabs = $el.find('.tab-wrapper').find('.tab');
$links.removeClass('active');
$link.addClass('active');
$tabs.removeClass('active');
$tabs.filter('[data-tab=' + targetTab + ']').addClass('active');
}
function changeTab(targetTab) {
var $link = $el.find('a[data-tab=' + targetTab + ']');
var $links = $link.closest('ul').find('a[data-tab]');
var $tabs = $el.find('.tab-wrapper').find('.tab');
$links.removeClass('active');
$link.addClass('active');
$tabs.removeClass('active');
$tabs.filter('[data-tab=' + targetTab + ']').addClass('active');
}
return {
init: init,
reinit: reinit,
render: render
};
return {
init: init,
reinit: reinit,
render: render
};
};

View File

@ -2,69 +2,84 @@ var App = App || {};
App.Promise = function(_, jQuery, progress) {
var active = [];
var promiseId = 0;
function BrokenPromiseError(promiseId) {
this.name = 'BrokenPromiseError';
this.message = 'Broken promise (promise ID: ' + promiseId + ')';
}
BrokenPromiseError.prototype = new Error();
function make(callback) {
var deferred = jQuery.Deferred();
var promise = deferred.promise();
promise.promiseId = ++ promiseId;
var active = [];
var promiseId = 0;
progress.start();
callback(function() {
try {
deferred.resolve.apply(deferred, arguments);
active = _.without(active, promise.promiseId);
progress.done();
} catch (e) {
progress.reset();
}
}, function() {
try {
deferred.reject.apply(deferred, arguments);
active = _.without(active, promise.promiseId);
progress.done();
} catch (e) {
progress.reset();
}
});
function make(callback, useProgress) {
var deferred = jQuery.Deferred();
var promise = deferred.promise();
promise.promiseId = ++ promiseId;
active.push(promise.promiseId);
if (useProgress === true) {
progress.start();
}
callback(function() {
try {
deferred.resolve.apply(deferred, arguments);
active = _.without(active, promise.promiseId);
progress.done();
} catch (e) {
if (!(e instanceof BrokenPromiseError)) {
console.log(e);
}
progress.reset();
}
}, function() {
try {
deferred.reject.apply(deferred, arguments);
active = _.without(active, promise.promiseId);
progress.done();
} catch (e) {
if (!(e instanceof BrokenPromiseError)) {
console.log(e);
}
progress.reset();
}
});
promise.always(function() {
if (!_.contains(active, promise.promiseId)) {
throw new Error('Broken promise (promise ID: ' + promise.promiseId + ')');
}
});
active.push(promise.promiseId);
return promise;
}
promise.always(function() {
if (!_.contains(active, promise.promiseId)) {
throw new BrokenPromiseError(promise.promiseId);
}
});
function wait() {
var promises = arguments;
var deferred = jQuery.Deferred();
return jQuery.when.apply(jQuery, promises)
.then(function() {
return deferred.resolve.apply(deferred, arguments);
}).fail(function() {
return deferred.reject.apply(deferred, arguments);
});
}
return promise;
}
function abortAll() {
active = [];
}
function wait() {
var promises = arguments;
var deferred = jQuery.Deferred();
return jQuery.when.apply(jQuery, promises)
.then(function() {
return deferred.resolve.apply(deferred, arguments);
}).fail(function() {
return deferred.reject.apply(deferred, arguments);
});
}
function getActive() {
return active.length;
}
function abortAll() {
active = [];
}
return {
make: make,
wait: wait,
getActive: getActive,
abortAll: abortAll,
};
function getActive() {
return active.length;
}
return {
make: function(callback) { return make(callback, true); },
makeSilent: function(callback) { return make(callback, false); },
wait: wait,
getActive: getActive,
abortAll: abortAll,
};
};

View File

@ -2,184 +2,184 @@ var App = App || {};
App.Router = function(_, jQuery, promise, util, appState, presenterManager) {
var root = '#/';
var previousLocation = window.location.href;
var routes = [];
var root = '#/';
var previousLocation = window.location.href;
var routes = [];
injectRoutes();
injectRoutes();
function injectRoutes() {
inject('', 'homePresenter');
inject('#/', 'homePresenter');
inject('#/404', 'httpErrorPresenter', {error: 404});
inject('#/home', 'homePresenter');
inject('#/login', 'loginPresenter');
inject('#/logout', 'logoutPresenter');
inject('#/register', 'registrationPresenter');
inject('#/upload', 'postUploadPresenter');
inject('#/password-reset(/:token)', 'userActivationPresenter', {operation: 'passwordReset'});
inject('#/activate(/:token)', 'userActivationPresenter', {operation: 'activation'});
inject('#/users(/:!query)', 'userListPresenter');
inject('#/user/:userName(/:tab)', 'userPresenter');
inject('#/posts(/:!query)', 'postListPresenter');
inject('#/post/:postNameOrId(/:!query)', 'postPresenter');
inject('#/comments(/:!query)', 'globalCommentListPresenter');
inject('#/tags(/:!query)', 'tagListPresenter');
inject('#/tag/:tagName', 'tagPresenter');
inject('#/help(/:tab)', 'helpPresenter');
inject('#/history(/:!query)', 'historyPresenter');
}
function injectRoutes() {
inject('', 'homePresenter');
inject('#/', 'homePresenter');
inject('#/404', 'httpErrorPresenter', {error: 404});
inject('#/home', 'homePresenter');
inject('#/login', 'loginPresenter');
inject('#/logout', 'logoutPresenter');
inject('#/register', 'registrationPresenter');
inject('#/upload', 'postUploadPresenter');
inject('#/password-reset(/:token)', 'userActivationPresenter', {operation: 'passwordReset'});
inject('#/activate(/:token)', 'userActivationPresenter', {operation: 'activation'});
inject('#/users(/:!query)', 'userListPresenter');
inject('#/user/:userName(/:tab)', 'userPresenter');
inject('#/posts(/:!query)', 'postListPresenter');
inject('#/post/:postNameOrId(/:!query)', 'postPresenter');
inject('#/comments(/:!query)', 'globalCommentListPresenter');
inject('#/tags(/:!query)', 'tagListPresenter');
inject('#/tag/:tagName', 'tagPresenter');
inject('#/help(/:tab)', 'helpPresenter');
inject('#/history(/:!query)', 'historyPresenter');
}
function navigate(url, useBrowserDispatcher) {
if (('pushState' in history) && !useBrowserDispatcher) {
history.pushState('', '', url);
dispatch();
} else {
window.location.href = url;
}
}
function navigate(url, useBrowserDispatcher) {
if (('pushState' in history) && !useBrowserDispatcher) {
history.pushState('', '', url);
dispatch();
} else {
window.location.href = url;
}
}
function navigateToMainPage() {
navigate(root);
}
function navigateToMainPage() {
navigate(root);
}
function navigateInplace(url, useBrowserDispatcher) {
if ('replaceState' in history) {
history.replaceState('', '', url);
if (!useBrowserDispatcher) {
dispatch();
} else {
location.reload();
}
} else {
navigate(url, useBrowserDispatcher);
}
}
function navigateInplace(url, useBrowserDispatcher) {
if ('replaceState' in history) {
history.replaceState('', '', url);
if (!useBrowserDispatcher) {
dispatch();
} else {
location.reload();
}
} else {
navigate(url, useBrowserDispatcher);
}
}
function start() {
if ('onhashchange' in window) {
window.onhashchange = dispatch;
} else {
window.onpopstate = dispatch;
}
dispatch();
}
function start() {
if ('onhashchange' in window) {
window.onhashchange = dispatch;
} else {
window.onpopstate = dispatch;
}
dispatch();
}
function inject(definition, presenterName, additionalParams) {
routes.push(new Route(definition, function(params) {
if (util.isExitConfirmationEnabled()) {
if (window.location.href === previousLocation) {
return;
} else {
if (window.confirm('Are you sure you want to leave this page? Data will be lost.')) {
util.disableExitConfirmation();
} else {
window.location.href = previousLocation;
return;
}
}
}
function inject(definition, presenterName, additionalParams) {
routes.push(new Route(definition, function(params) {
if (util.isExitConfirmationEnabled()) {
if (window.location.href === previousLocation) {
return;
} else {
if (window.confirm('Are you sure you want to leave this page? Data will be lost.')) {
util.disableExitConfirmation();
} else {
window.location.href = previousLocation;
return;
}
}
}
params = _.extend({}, params, additionalParams, {previousLocation: previousLocation});
params = _.extend({}, params, additionalParams, {previousLocation: previousLocation});
//abort every operation that can be executed
promise.abortAll();
previousLocation = window.location.href;
//abort every operation that can be executed
promise.abortAll();
previousLocation = window.location.href;
var presenter = App.DI.get(presenterName);
presenter.name = presenterName;
presenterManager.switchContentPresenter(presenter, params);
}));
}
var presenter = App.DI.get(presenterName);
presenter.name = presenterName;
presenterManager.switchContentPresenter(presenter, params);
}));
}
function dispatch() {
var url = document.location.hash;
for (var i = 0; i < routes.length; i ++) {
var route = routes[i];
if (route.match(url)) {
route.callback(route.params);
return true;
}
}
navigateInplace('#/404', true);
return false;
}
function dispatch() {
var url = decodeURI(document.location.hash);
for (var i = 0; i < routes.length; i ++) {
var route = routes[i];
if (route.match(url)) {
route.callback(route.params);
return true;
}
}
navigateInplace('#/404', true);
return false;
}
function parseComplexParamValue(value) {
var result = {};
var params = (value || '').split(/;/);
for (var i = 0; i < params.length; i ++) {
var param = params[i];
if (!param) {
continue;
}
var kv = param.split(/=/);
result[kv[0]] = kv[1];
}
return result;
}
function parseComplexParamValue(value) {
var result = {};
var params = (value || '').split(/;/);
for (var i = 0; i < params.length; i ++) {
var param = params[i];
if (!param) {
continue;
}
var kv = param.split(/=/);
result[kv[0]] = kv[1];
}
return result;
}
function Route(definition, callback) {
var possibleRoutes = getPossibleRoutes(definition);
function Route(definition, callback) {
var possibleRoutes = getPossibleRoutes(definition);
function getPossibleRoutes(routeDefinition) {
var parts = [];
var re = new RegExp('\\(([^}]+?)\\)', 'g');
while (true) {
var text = re.exec(routeDefinition);
if (!text) {
break;
}
parts.push(text[1]);
}
var possibleRoutes = [routeDefinition.split('(')[0]];
for (var i = 0; i < parts.length; i ++) {
possibleRoutes.push(possibleRoutes[possibleRoutes.length - 1] + parts[i]);
}
return possibleRoutes;
}
function getPossibleRoutes(routeDefinition) {
var parts = [];
var re = new RegExp('\\(([^}]+?)\\)', 'g');
while (true) {
var text = re.exec(routeDefinition);
if (!text) {
break;
}
parts.push(text[1]);
}
var possibleRoutes = [routeDefinition.split('(')[0]];
for (var i = 0; i < parts.length; i ++) {
possibleRoutes.push(possibleRoutes[possibleRoutes.length - 1] + parts[i]);
}
return possibleRoutes;
}
function match(url) {
var params = {};
for (var i = 0; i < possibleRoutes.length; i ++) {
var possibleRoute = possibleRoutes[i];
var compare = url;
var possibleRouteParts = possibleRoute.split('/');
var compareParts = compare.split('/');
if (possibleRoute.search(':') > 0) {
for (var j = 0; j < possibleRouteParts.length; j ++) {
if ((j < compareParts.length) && (possibleRouteParts[j].charAt(0) === ':')) {
var key = possibleRouteParts[j].substring(1);
var value = compareParts[j];
if (key.charAt(0) === '!') {
key = key.substring(1);
value = parseComplexParamValue(value);
}
params[key] = value;
compareParts[j] = possibleRouteParts[j];
compare = compareParts.join('/');
}
}
}
if (possibleRoute === compare) {
this.params = params;
return true;
}
}
return false;
}
function match(url) {
var params = {};
for (var i = 0; i < possibleRoutes.length; i ++) {
var possibleRoute = possibleRoutes[i];
var compare = url;
var possibleRouteParts = possibleRoute.split('/');
var compareParts = compare.split('/');
if (possibleRoute.search(':') > 0) {
for (var j = 0; j < possibleRouteParts.length; j ++) {
if ((j < compareParts.length) && (possibleRouteParts[j].charAt(0) === ':')) {
var key = possibleRouteParts[j].substring(1);
var value = compareParts[j];
if (key.charAt(0) === '!') {
key = key.substring(1);
value = parseComplexParamValue(value);
}
params[key] = value;
compareParts[j] = possibleRouteParts[j];
compare = compareParts.join('/');
}
}
}
if (possibleRoute === compare) {
this.params = params;
return true;
}
}
return false;
}
this.match = match;
this.callback = callback;
}
this.match = match;
this.callback = callback;
}
return {
start: start,
navigate: navigate,
navigateInplace: navigateInplace,
navigateToMainPage: navigateToMainPage,
};
return {
start: start,
navigate: navigate,
navigateInplace: navigateInplace,
navigateToMainPage: navigateToMainPage,
};
};
App.DI.registerSingleton('router', ['_', 'jQuery', 'promise', 'util', 'appState', 'presenterManager'], App.Router);

View File

@ -3,75 +3,83 @@ App.Services = App.Services || {};
App.Services.PostsAroundCalculator = function(_, promise, util, pager) {
pager.init({url: '/posts'});
pager.init({url: '/posts'});
function resetCache() {
pager.resetCache();
}
function resetCache() {
pager.resetCache();
}
function getLinksToPostsAround(query, postId) {
return promise.make(function(resolve, reject) {
pager.setSearchParams(query);
pager.setPage(query.page);
promise.wait(pager.retrieveCached())
.then(function(response) {
var postIds = _.pluck(response.entities, 'id');
var position = _.indexOf(postIds, postId);
function getLinksToPostsAround(query, postId) {
return promise.make(function(resolve, reject) {
pager.setSearchParams(query);
pager.setPage(query.page);
promise.wait(pager.retrieveCached())
.then(function(response) {
var postIds = _.pluck(response.json.posts, 'id');
var position = _.indexOf(postIds, postId);
if (position === -1) {
resolve(null, null);
}
if (position === -1) {
resolve(null, null);
}
promise.wait(
getLinkToPostAround(postIds, position, query.page, -1),
getLinkToPostAround(postIds, position, query.page, 1))
.then(function(nextPostUrl, prevPostUrl) {
resolve(nextPostUrl, prevPostUrl);
}).fail(function() {
reject();
});
}).fail(function() {
reject();
});
});
}
promise.wait(
getLinkToPostAround(postIds, position, query.page, -1),
getLinkToPostAround(postIds, position, query.page, 1))
.then(function(nextPostUrl, prevPostUrl) {
resolve(nextPostUrl, prevPostUrl);
}).fail(function() {
reject();
});
}).fail(function() {
reject();
});
});
}
function getLinkToPostAround(postIds, position, page, direction) {
return promise.make(function(resolve, reject) {
if (position + direction >= 0 && position + direction < postIds.length) {
var url = util.appendComplexRouteParam(
'#/post/' + postIds[position + direction],
_.extend({page: page}, pager.getSearchParams()));
resolve(url);
} else if (page + direction >= 1) {
pager.setPage(page + direction);
promise.wait(pager.retrieveCached())
.then(function(response) {
if (response.entities.length) {
var post = direction === - 1 ?
_.last(response.entities) :
_.first(response.entities);
function getLinkToPostAround(postIds, position, page, direction) {
return promise.make(function(resolve, reject) {
if (position + direction >= 0 && position + direction < postIds.length) {
var url = util.appendComplexRouteParam(
'#/post/' + postIds[position + direction],
util.simplifySearchQuery(
_.extend(
{page: page},
pager.getSearchParams())));
var url = util.appendComplexRouteParam(
'#/post/' + post.id,
_.extend({page: page + direction}, pager.getSearchParams()));
resolve(url);
} else {
resolve(null);
}
}).fail(function() {
reject();
});
} else {
resolve(null);
}
});
}
resolve(url);
} else if (page + direction >= 1) {
pager.setPage(page + direction);
promise.wait(pager.retrieveCached())
.then(function(response) {
if (response.json.posts.length) {
var post = direction === - 1 ?
_.last(response.json.posts) :
_.first(response.json.posts);
return {
resetCache: resetCache,
getLinksToPostsAround: getLinksToPostsAround,
};
var url = util.appendComplexRouteParam(
'#/post/' + post.id,
util.simplifySearchQuery(
_.extend(
{page: page + direction},
pager.getSearchParams())));
resolve(url);
} else {
resolve(null);
}
}).fail(function() {
reject();
});
} else {
resolve(null);
}
});
}
return {
resetCache: resetCache,
getLinksToPostsAround: getLinksToPostsAround,
};
};
App.DI.register('postsAroundCalculator', ['_', 'promise', 'util', 'pager'], App.Services.PostsAroundCalculator);

View File

@ -2,31 +2,31 @@ var App = App || {};
App.Services = App.Services || {};
App.Services.TagList = function(jQuery) {
var tags = [];
var tags = [];
function refreshTags() {
jQuery.ajax({
success: function(data, textStatus, xhr) {
tags = data;
},
error: function(xhr, textStatus, errorThrown) {
console.log(new Error(errorThrown));
},
type: 'GET',
url: '/data/tags.json',
});
}
function refreshTags() {
jQuery.ajax({
success: function(data, textStatus, xhr) {
tags = data;
},
error: function(xhr, textStatus, errorThrown) {
console.log(new Error(errorThrown));
},
type: 'GET',
url: '/data/tags.json',
});
}
function getTags() {
return tags;
}
function getTags() {
return tags;
}
refreshTags();
refreshTags();
return {
refreshTags: refreshTags,
getTags: getTags,
};
return {
refreshTags: refreshTags,
getTags: getTags,
};
};
App.DI.registerSingleton('tagList', ['jQuery'], App.Services.TagList);

View File

@ -2,50 +2,50 @@ var App = App || {};
App.State = function() {
var properties = {};
var observers = {};
var properties = {};
var observers = {};
function get(key) {
return properties[key];
}
function get(key) {
return properties[key];
}
function set(key, value) {
properties[key] = value;
if (key in observers) {
for (var observerName in observers[key]) {
if (observers[key].hasOwnProperty(observerName)) {
observers[key][observerName](key, value);
}
}
}
}
function set(key, value) {
properties[key] = value;
if (key in observers) {
for (var observerName in observers[key]) {
if (observers[key].hasOwnProperty(observerName)) {
observers[key][observerName](key, value);
}
}
}
}
function startObserving(key, observerName, callback) {
if (!(key in observers)) {
observers[key] = {};
}
if (!(observerName in observers[key])) {
observers[key][observerName] = {};
}
observers[key][observerName] = callback;
}
function startObserving(key, observerName, callback) {
if (!(key in observers)) {
observers[key] = {};
}
if (!(observerName in observers[key])) {
observers[key][observerName] = {};
}
observers[key][observerName] = callback;
}
function stopObserving(key, observerName) {
if (!(key in observers)) {
return;
}
if (!(observerName in observers[key])) {
return;
}
delete observers[key][observerName];
}
function stopObserving(key, observerName) {
if (!(key in observers)) {
return;
}
if (!(observerName in observers[key])) {
return;
}
delete observers[key][observerName];
}
return {
get: get,
set: set,
startObserving: startObserving,
stopObserving: stopObserving,
};
return {
get: get,
set: set,
startObserving: startObserving,
stopObserving: stopObserving,
};
};

View File

@ -2,82 +2,146 @@ var App = App || {};
App.Util = App.Util || {};
App.Util.Draggable = function(jQuery) {
function relativeDragStrategy($element) {
var $parent = $element.parent();
var delta;
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_RIGHT = 39;
var KEY_DOWN = 40;
return {
click: function(e) {
delta = {
x: $element.offset().left - e.clientX,
y: $element.offset().top - e.clientY,
};
},
function relativeDragStrategy($element) {
var $parent = $element.parent();
var delta;
var x = $element.offset().left - $parent.offset().left;
var y = $element.offset().top - $parent.offset().top;
update: function(e) {
var x = e.clientX + delta.x - $parent.offset().left;
var y = e.clientY + delta.y - $parent.offset().top;
x = Math.min(Math.max(x, 0), $parent.outerWidth() - $element.outerWidth());
y = Math.min(Math.max(y, 0), $parent.outerHeight() - $element.outerHeight());
x *= 100.0 / $parent.outerWidth();
y *= 100.0 / $parent.outerHeight();
$element.css({
left: x + '%',
top: y + '%'});
},
};
}
var getPosition = function() {
return {x: x, y: y};
};
function absoluteDragStrategy($element) {
var delta;
var setPosition = function(newX, newY) {
x = newX;
y = newY;
var screenX = Math.min(Math.max(newX, 0), $parent.outerWidth() - $element.outerWidth());
var screenY = Math.min(Math.max(newY, 0), $parent.outerHeight() - $element.outerHeight());
screenX *= 100.0 / $parent.outerWidth();
screenY *= 100.0 / $parent.outerHeight();
$element.css({
left: screenX + '%',
top: screenY + '%'});
};
return {
click: function(e) {
delta = {
x: $element.position().left - e.clientX,
y: $element.position().top - e.clientY,
};
},
return {
mouseClicked: function(e) {
delta = {
x: $element.offset().left - e.clientX,
y: $element.offset().top - e.clientY,
};
},
update: function(e) {
var x = e.clientX + delta.x;
var y = e.clientY + delta.y;
$element.css({
left: x + 'px',
top: y + 'px'});
},
};
}
mouseMoved: function(e) {
setPosition(
e.clientX + delta.x - $parent.offset().left,
e.clientY + delta.y - $parent.offset().top);
},
function makeDraggable($element, dragStrategy) {
var strategy = dragStrategy($element);
getPosition: getPosition,
setPosition: setPosition,
};
}
$element.addClass('draggable');
$element.mousedown(function(e) {
if (e.target !== $element.get(0)) {
return;
}
e.preventDefault();
$element.addClass('dragging');
function absoluteDragStrategy($element) {
var delta;
var x = $element.offset().left;
var y = $element.offset().top;
strategy.click(e);
jQuery(window).bind('mousemove.elemmove', function(e) {
strategy.update(e);
}).bind('mouseup.elemmove', function(e) {
e.preventDefault();
strategy.update(e);
$element.removeClass('dragging');
jQuery(window).unbind('mousemove.elemmove');
jQuery(window).unbind('mouseup.elemmove');
});
});
}
var getPosition = function() {
return {x: x, y: y};
};
return {
makeDraggable: makeDraggable,
absoluteDragStrategy: absoluteDragStrategy,
relativeDragStrategy: relativeDragStrategy,
};
var setPosition = function(newX, newY) {
x = newX;
y = newY;
$element.css({
left: x + 'px',
top: y + 'px'});
};
return {
mouseClicked: function(e) {
delta = {
x: $element.position().left - e.clientX,
y: $element.position().top - e.clientY,
};
},
mouseMoved: function(e) {
setPosition(e.clientX + delta.x, e.clientY + delta.y);
},
getPosition: getPosition,
setPosition: setPosition,
};
}
function makeDraggable($element, dragStrategy, enableHotkeys) {
var strategy = dragStrategy($element);
$element.data('drag-strategy', strategy);
$element.addClass('draggable');
$element.mousedown(function(e) {
if (e.target !== $element.get(0)) {
return;
}
e.preventDefault();
$element.focus();
$element.addClass('dragging');
strategy.mouseClicked(e);
jQuery(window).bind('mousemove.elemmove', function(e) {
strategy.mouseMoved(e);
}).bind('mouseup.elemmove', function(e) {
e.preventDefault();
strategy.mouseMoved(e);
$element.removeClass('dragging');
jQuery(window).unbind('mousemove.elemmove');
jQuery(window).unbind('mouseup.elemmove');
});
});
if (enableHotkeys) {
$element.keydown(function(e) {
var position = strategy.getPosition();
var oldPosition = {x: position.x, y: position.y};
if (e.shiftKey) {
return;
}
var delta = e.ctrlKey ? 10 : 1;
if (e.which === KEY_LEFT) {
position.x -= delta;
} else if (e.which === KEY_RIGHT) {
position.x += delta;
} else if (e.which === KEY_UP) {
position.y -= delta;
} else if (e.which === KEY_DOWN) {
position.y += delta;
}
if (position.x !== oldPosition.x || position.y !== oldPosition.y) {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
strategy.setPosition(position.x, position.y);
}
});
}
}
return {
makeDraggable: makeDraggable,
absoluteDragStrategy: absoluteDragStrategy,
relativeDragStrategy: relativeDragStrategy,
};
};

View File

@ -3,245 +3,273 @@ App.Util = App.Util || {};
App.Util.Misc = function(_, jQuery, marked, promise) {
var exitConfirmationEnabled = false;
var exitConfirmationEnabled = false;
function transparentPixel() {
return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
}
function transparentPixel() {
return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
}
function enableExitConfirmation() {
exitConfirmationEnabled = true;
jQuery(window).bind('beforeunload', function(e) {
return 'There are unsaved changes.';
});
}
function enableExitConfirmation() {
exitConfirmationEnabled = true;
jQuery(window).bind('beforeunload', function(e) {
return 'There are unsaved changes.';
});
}
function disableExitConfirmation() {
exitConfirmationEnabled = false;
jQuery(window).unbind('beforeunload');
}
function disableExitConfirmation() {
exitConfirmationEnabled = false;
jQuery(window).unbind('beforeunload');
}
function isExitConfirmationEnabled() {
return exitConfirmationEnabled;
}
function isExitConfirmationEnabled() {
return exitConfirmationEnabled;
}
function loadImagesNicely($img) {
if (!$img.get(0).complete) {
$img.addClass('loading');
$img.css({opacity: 0});
var $div = jQuery('<div>Loading ' + $img.attr('alt') + '&hellip;</div>');
var width = $img.width();
var height = $img.height();
if (width > 50 && height > 50) {
$div.css({
position: 'absolute',
width: width + 'px',
height: height + 'px',
color: 'rgba(0, 0, 0, 0.15)',
zIndex: -1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'});
$div.insertBefore($img);
$div.offset($img.offset());
}
$img.bind('load', function() {
$img.animate({opacity: 1}, 'fast');
$img.removeClass('loading');
$div.fadeOut($div.remove);
});
}
}
function loadImagesNicely($img) {
if (!$img.get(0).complete) {
$img.addClass('loading');
$img.css({opacity: 0});
var $div = jQuery('<div>Loading ' + $img.attr('alt') + '&hellip;</div>');
var width = $img.width();
var height = $img.height();
if (width > 50 && height > 50) {
$div.css({
position: 'absolute',
width: width + 'px',
height: height + 'px',
color: 'rgba(0, 0, 0, 0.15)',
zIndex: -1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'});
$div.insertBefore($img);
$div.offset($img.offset());
}
$img.bind('load', function() {
$img.animate({opacity: 1}, 'fast');
$img.removeClass('loading');
$div.fadeOut($div.remove);
});
}
}
function promiseTemplate(templateName) {
return promiseTemplateFromDOM(templateName) ||
promiseTemplateWithAJAX(templateName);
}
function promiseTemplate(templateName) {
return promiseTemplateFromDOM(templateName) ||
promiseTemplateWithAJAX(templateName);
}
function promiseTemplateFromDOM(templateName) {
var $template = jQuery('#' + templateName + '-template');
if ($template.length) {
return promise.make(function(resolve, reject) {
resolve(_.template($template.html()));
});
}
return null;
}
function promiseTemplateFromDOM(templateName) {
var $template = jQuery('#' + templateName + '-template');
if ($template.length) {
return promise.make(function(resolve, reject) {
resolve(_.template($template.html()));
});
}
return null;
}
function promiseTemplateWithAJAX(templateName) {
return promise.make(function(resolve, reject) {
var templatesDir = '/templates';
var templateUrl = templatesDir + '/' + templateName + '.tpl';
function promiseTemplateWithAJAX(templateName) {
return promise.make(function(resolve, reject) {
var templatesDir = '/templates';
var templateUrl = templatesDir + '/' + templateName + '.tpl';
jQuery.ajax({
url: templateUrl,
method: 'GET',
success: function(data, textStatus, xhr) {
resolve(_.template(data));
},
error: function(xhr, textStatus, errorThrown) {
console.log(new Error('Error while loading template ' + templateName + ': ' + errorThrown));
reject();
},
});
});
}
jQuery.ajax({
url: templateUrl,
method: 'GET',
success: function(data, textStatus, xhr) {
resolve(_.template(data));
},
error: function(xhr, textStatus, errorThrown) {
console.log(new Error('Error while loading template ' + templateName + ': ' + errorThrown));
reject();
},
});
});
}
function formatRelativeTime(timeString) {
if (!timeString) {
return 'never';
}
function formatRelativeTime(timeString) {
if (!timeString) {
return 'never';
}
var then = Date.parse(timeString);
var now = Date.now();
var difference = Math.abs(now - then);
var future = now < then;
var then = Date.parse(timeString);
var now = Date.now();
var difference = Math.abs(now - then);
var future = now < then;
var text = (function(difference) {
var mul = 1000;
var prevMul;
var text = (function(difference) {
var mul = 1000;
var prevMul;
mul *= 60;
if (difference < mul) {
return 'a few seconds';
} else if (difference < mul * 2) {
return 'a minute';
}
mul *= 60;
if (difference < mul) {
return 'a few seconds';
} else if (difference < mul * 2) {
return 'a minute';
}
prevMul = mul; mul *= 60;
if (difference < mul) {
return Math.round(difference / prevMul) + ' minutes';
} else if (difference < mul * 2) {
return 'an hour';
}
prevMul = mul; mul *= 60;
if (difference < mul) {
return Math.round(difference / prevMul) + ' minutes';
} else if (difference < mul * 2) {
return 'an hour';
}
prevMul = mul; mul *= 24;
if (difference < mul) {
return Math.round(difference / prevMul) + ' hours';
} else if (difference < mul * 2) {
return 'a day';
}
prevMul = mul; mul *= 24;
if (difference < mul) {
return Math.round(difference / prevMul) + ' hours';
} else if (difference < mul * 2) {
return 'a day';
}
prevMul = mul; mul *= 30.42;
if (difference < mul) {
return Math.round(difference / prevMul) + ' days';
} else if (difference < mul * 2) {
return 'a month';
}
prevMul = mul; mul *= 30.42;
if (difference < mul) {
return Math.round(difference / prevMul) + ' days';
} else if (difference < mul * 2) {
return 'a month';
}
prevMul = mul; mul *= 12;
if (difference < mul) {
return Math.round(difference / prevMul) + ' months';
} else if (difference < mul * 2) {
return 'a year';
}
prevMul = mul; mul *= 12;
if (difference < mul) {
return Math.round(difference / prevMul) + ' months';
} else if (difference < mul * 2) {
return 'a year';
}
return Math.round(difference / mul) + ' years';
})(difference);
return Math.round(difference / mul) + ' years';
})(difference);
if (text === 'a day') {
return future ? 'tomorrow' : 'yesterday';
}
return future ? 'in ' + text : text + ' ago';
}
if (text === 'a day') {
return future ? 'tomorrow' : 'yesterday';
}
return future ? 'in ' + text : text + ' ago';
}
function formatUnits(number, base, suffixes, callback) {
if (!number && number !== 0) {
return NaN;
}
number *= 1.0;
function formatAbsoluteTime(timeString) {
var time = new Date(Date.parse(timeString));
return time.toString();
}
var suffix = suffixes.shift();
while (number >= base && suffixes.length > 0) {
suffix = suffixes.shift();
number /= base;
}
function formatUnits(number, base, suffixes, callback) {
if (!number && number !== 0) {
return NaN;
}
number *= 1.0;
if (typeof(callback) === 'undefined') {
callback = function(number, suffix) {
return suffix ? number.toFixed(1) + suffix : number;
};
}
var suffix = suffixes.shift();
while (number >= base && suffixes.length > 0) {
suffix = suffixes.shift();
number /= base;
}
return callback(number, suffix);
}
if (typeof(callback) === 'undefined') {
callback = function(number, suffix) {
return suffix ? number.toFixed(1) + suffix : number;
};
}
function formatFileSize(fileSize) {
return formatUnits(
fileSize,
1024,
['B', 'K', 'M', 'G'],
function(number, suffix) {
var decimalPlaces = number < 20 && suffix !== 'B' ? 1 : 0;
return number.toFixed(decimalPlaces) + suffix;
});
}
return callback(number, suffix);
}
function formatMarkdown(text) {
var renderer = new marked.Renderer();
function formatFileSize(fileSize) {
return formatUnits(
fileSize,
1024,
['B', 'K', 'M', 'G'],
function(number, suffix) {
var decimalPlaces = number < 20 && suffix !== 'B' ? 1 : 0;
return number.toFixed(decimalPlaces) + suffix;
});
}
var options = {
renderer: renderer,
breaks: true,
sanitize: true,
smartypants: true,
};
function formatMarkdown(text) {
var renderer = new marked.Renderer();
var preDecorator = function(text) {
//prevent ^#... from being treated as headers, due to tag permalinks
text = text.replace(/^#/g, '%%%#');
//fix \ before ~ being stripped away
text = text.replace(/\\~/g, '%%%T');
return text;
};
var options = {
renderer: renderer,
breaks: true,
sanitize: true,
smartypants: true,
};
var postDecorator = function(text) {
//restore fixes
text = text.replace(/%%%T/g, '\\~');
text = text.replace(/%%%#/g, '#');
var sjis = [];
//search permalinks
text = text.replace(/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig, '<a href="#/posts/query=$1"><code>$1</code></a>');
//spoilers
text = text.replace(/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig, '<span class="spoiler">$1</span>');
//strike-through
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>');
text = text.replace(/\\~/g, '~');
//post premalinks
text = text.replace(/(^|[\s<>\(\)\[\]])@(\d+)/g, '$1<a href="#/post/$2"><code>@$2</code></a>');
//user permalinks
text = text.replace(/(^|[\s<>\(\)\[\]])\+([a-zA-Z0-9_-]+)/g, '$1<a href="#/user/$2"><code>+$2</code></a>');
//tag permalinks
text = text.replace(/(^|[\s<>\(\)\[\]])\#([^\s<>/\\]+)/g, '$1<a href="#/posts/query=$2"><code>#$2</code></a>');
return text;
};
var preDecorator = function(text) {
text = text.replace(/\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig, function(match, capture) {
var ret = '%%%SJIS' + sjis.length;
sjis.push(capture);
return ret;
});
//prevent ^#... from being treated as headers, due to tag permalinks
text = text.replace(/^#/g, '%%%#');
//fix \ before ~ being stripped away
text = text.replace(/\\~/g, '%%%T');
//post, user and tags premalinks
text = text.replace(/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g, '$1[$2]($2)');
text = text.replace(/\]\(@(\d+)\)/g, '](#/post/$1)');
text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](#/user/$1)');
text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](#/posts/query=$1)');
return text;
};
return postDecorator(marked(preDecorator(text), options));
}
var postDecorator = function(text) {
//restore fixes
text = text.replace(/%%%T/g, '\\~');
text = text.replace(/%%%#/g, '#');
function appendComplexRouteParam(baseUri, params) {
var result = baseUri + '/';
_.each(params, function(v, k) {
if (typeof(v) !== 'undefined') {
result += k + '=' + v + ';';
}
});
return result.slice(0, -1);
}
text = text.replace(/%%%SJIS(\d+)/, function(match, capture) { return '<div class="sjis">' + sjis[capture] + '</div>'; });
return {
promiseTemplate: promiseTemplate,
formatRelativeTime: formatRelativeTime,
formatFileSize: formatFileSize,
formatMarkdown: formatMarkdown,
enableExitConfirmation: enableExitConfirmation,
disableExitConfirmation: disableExitConfirmation,
isExitConfirmationEnabled: isExitConfirmationEnabled,
transparentPixel: transparentPixel,
loadImagesNicely: loadImagesNicely,
appendComplexRouteParam: appendComplexRouteParam,
};
//search permalinks
text = text.replace(/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig, '<a href="#/posts/query=$1"><code>$1</code></a>');
//spoilers
text = text.replace(/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig, '<span class="spoiler">$1</span>');
//[small]
text = text.replace(/\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig, '<small>$1</small>');
//strike-through
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>');
text = text.replace(/\\~/g, '~');
return text;
};
return postDecorator(marked(preDecorator(text), options));
}
function appendComplexRouteParam(baseUri, params) {
var result = baseUri + '/';
_.each(params, function(v, k) {
if (typeof(v) !== 'undefined') {
result += k + '=' + v + ';';
}
});
return result.slice(0, -1);
}
function simplifySearchQuery(query) {
if (typeof(query) === 'undefined') {
return {};
}
if (query.page === 1) {
delete query.page;
}
query = _.pick(query, _.identity); //remove falsy values
return query;
}
return {
promiseTemplate: promiseTemplate,
formatRelativeTime: formatRelativeTime,
formatAbsoluteTime: formatAbsoluteTime,
formatFileSize: formatFileSize,
formatMarkdown: formatMarkdown,
enableExitConfirmation: enableExitConfirmation,
disableExitConfirmation: disableExitConfirmation,
isExitConfirmationEnabled: isExitConfirmationEnabled,
transparentPixel: transparentPixel,
loadImagesNicely: loadImagesNicely,
appendComplexRouteParam: appendComplexRouteParam,
simplifySearchQuery: simplifySearchQuery,
};
};

View File

@ -2,46 +2,108 @@ var App = App || {};
App.Util = App.Util || {};
App.Util.Resizable = function(jQuery) {
function makeResizable($element) {
var $resizer = jQuery('<div class="resizer"></div>');
$element.append($resizer);
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_RIGHT = 39;
var KEY_DOWN = 40;
$resizer.mousedown(function(e) {
e.preventDefault();
e.stopPropagation();
$element.addClass('resizing');
function relativeResizeStrategy($element) {
var $parent = $element.parent();
var delta;
var width = $element.width();
var height = $element.height();
var $parent = $element.parent();
var deltaX = $element.width() - e.clientX;
var deltaY = $element.height() - e.clientY;
var getSize = function() {
return {width: width, height: height};
};
var update = function(e) {
var w = e.clientX + deltaX;
var h = e.clientY + deltaY;
w = Math.min(Math.max(w, 20), $parent.outerWidth() + $parent.offset().left - $element.offset().left);
h = Math.min(Math.max(h, 20), $parent.outerHeight() + $parent.offset().top - $element.offset().top);
w *= 100.0 / $parent.outerWidth();
h *= 100.0 / $parent.outerHeight();
$element.css({
width: w + '%',
height: h + '%'});
};
var setSize = function(newWidth, newHeight) {
width = newWidth;
height = newHeight;
var screenWidth = Math.min(Math.max(width, 20), $parent.outerWidth() + $parent.offset().left - $element.offset().left);
var screenHeight = Math.min(Math.max(height, 20), $parent.outerHeight() + $parent.offset().top - $element.offset().top);
screenWidth *= 100.0 / $parent.outerWidth();
screenHeight *= 100.0 / $parent.outerHeight();
$element.css({
width: screenWidth + '%',
height: screenHeight + '%'});
};
jQuery(window).bind('mousemove.elemsize', function(e) {
update(e);
}).bind('mouseup.elemsize', function(e) {
e.preventDefault();
update(e);
$element.removeClass('resizing');
jQuery(window).unbind('mousemove.elemsize');
jQuery(window).unbind('mouseup.elemsize');
});
});
}
return {
mouseClicked: function(e) {
delta = {
x: $element.width() - e.clientX,
y: $element.height() - e.clientY,
};
},
return {
makeResizable: makeResizable,
};
mouseMoved: function(e) {
setSize(
e.clientX + delta.x,
e.clientY + delta.y);
},
getSize: getSize,
setSize: setSize,
};
}
function makeResizable($element, enableHotkeys) {
var $resizer = jQuery('<div class="resizer"></div>');
var strategy = relativeResizeStrategy($element);
$element.append($resizer);
$resizer.mousedown(function(e) {
e.preventDefault();
e.stopPropagation();
$element.focus();
$element.addClass('resizing');
strategy.mouseClicked(e);
jQuery(window).bind('mousemove.elemsize', function(e) {
strategy.mouseMoved(e);
}).bind('mouseup.elemsize', function(e) {
e.preventDefault();
strategy.mouseMoved(e);
$element.removeClass('resizing');
jQuery(window).unbind('mousemove.elemsize');
jQuery(window).unbind('mouseup.elemsize');
});
});
if (enableHotkeys) {
$element.keydown(function(e) {
var size = strategy.getSize();
var oldSize = {width: size.width, height: size.height};
if (!e.shiftKey) {
return;
}
var delta = e.ctrlKey ? 10 : 1;
if (e.which === KEY_LEFT) {
size.width -= delta;
} else if (e.which === KEY_RIGHT) {
size.width += delta;
} else if (e.which === KEY_UP) {
size.height -= delta;
} else if (e.which === KEY_DOWN) {
size.height += delta;
}
if (size.width !== oldSize.width || size.height !== oldSize.height) {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
strategy.setSize(size.width, size.height);
}
});
}
}
return {
makeResizable: makeResizable,
};
};

View File

@ -1,5 +1,5 @@
<div class="http-error">
<img src="/img/404.png" alt="404 Not found"/>
<br/>
<a href="#/">Back to main page</a>
<img src="/img/404.png" alt="404 Not found"/>
<br/>
<a href="#/">Back to main page</a>
</div>

View File

@ -1,20 +1,20 @@
<div class="messages"></div>
<form class="form-wrapper account-removal">
<div class="form-row">
<label class="form-label" for="account-removal-confirmation">Confirmation:</label>
<div class="form-input">
<input type="checkbox" id="account-removal-confirmation" name="confirmation"/>
<label for="account-removal-confirmation">
I confirm that I want to delete this account.
</label>
</div>
</div>
<div class="form-row">
<label class="form-label" for="account-removal-confirmation">Confirmation:</label>
<div class="form-input">
<input type="checkbox" id="account-removal-confirmation" name="confirmation"/>
<label for="account-removal-confirmation">
I confirm that I want to delete this account.
</label>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Delete account</button>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Delete account</button>
</div>
</div>
</form>

View File

@ -1,114 +1,114 @@
<div class="messages"></div>
<form class="form-wrapper account-settings">
<% if (canChangeAvatarStyle) { %>
<div class="form-row">
<label class="form-label">User picture:</label>
<div class="form-input">
<%
var avatarStyles = {
gravatar: 'Gravatar',
manual: 'Custom',
blank: 'Blank',
};
%>
<% _.each(avatarStyles, function(v, k) { %>
<input <% print(user.avatarStyle == k ? 'checked="checked"' : '') %> type="radio" name="avatar-style" id="account-settings-avatar-<%= k %>" value="<%= k %>"/>
<label for="account-settings-avatar-<%= k %>">
<%= v %>
</label>
<% }) %>
</div>
</div>
<% if (canChangeAvatarStyle) { %>
<div class="form-row">
<label class="form-label">User picture:</label>
<div class="form-input">
<%
var avatarStyles = {
gravatar: 'Gravatar',
manual: 'Custom',
blank: 'Blank',
};
%>
<% _.each(avatarStyles, function(v, k) { %>
<input <% print(user.avatarStyle == k ? 'checked="checked"' : '') %> type="radio" name="avatar-style" id="account-settings-avatar-<%= k %>" value="<%= k %>"/>
<label for="account-settings-avatar-<%= k %>">
<%= v %>
</label>
<% }) %>
</div>
</div>
<div class="form-row avatar-content">
<label class="form-label"></label>
<div class="form-input">
<input type="file" name="avatar-content" id="account-settings-avatar-content"/>
</div>
</div>
<% } %>
<div class="form-row avatar-content">
<label class="form-label"></label>
<div class="form-input">
<input type="file" name="avatar-content" id="account-settings-avatar-content"/>
</div>
</div>
<% } %>
<% if (canChangeName) { %>
<div class="form-row">
<label class="form-label" for="account-settings-name">Name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="userName" id="account-settings-name" placeholder="New name&hellip;" value="<%= user.name %>"/>
</div>
</div>
<% } %>
<% if (canChangeName) { %>
<div class="form-row">
<label class="form-label" for="account-settings-name">Name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="userName" id="account-settings-name" placeholder="New name&hellip;" value="<%= user.name %>"/>
</div>
</div>
<% } %>
<% if (canChangeEmailAddress) { %>
<div class="form-row">
<label class="form-label" for="account-settings-email">E-mail:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="email" id="account-settings-email" placeholder="New e-mail&hellip;" value="<%= user.email %>"/>
<% if (user.emailUnconfirmed) { %>
<br/>
<span class="account-settings-email-unconfirmed">(unconfirmed) <%= user.emailUnconfirmed %></span>
<% } %>
</div>
</div>
<% } %>
<% if (canChangeEmailAddress) { %>
<div class="form-row">
<label class="form-label" for="account-settings-email">E-mail:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="email" id="account-settings-email" placeholder="New e-mail&hellip;" value="<%= user.email %>"/>
<% if (user.emailUnconfirmed) { %>
<br/>
<span class="account-settings-email-unconfirmed">(unconfirmed) <%= user.emailUnconfirmed %></span>
<% } %>
</div>
</div>
<% } %>
<% if (canChangePassword) { %>
<div class="form-row">
<label class="form-label" for="account-settings-password">New password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password" id="account-settings-password" placeholder="New password&hellip;" value=""/>
</div>
</div>
<% if (canChangePassword) { %>
<div class="form-row">
<label class="form-label" for="account-settings-password">New password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password" id="account-settings-password" placeholder="New password&hellip;" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="account-settings-password-confirmation"></label>
<div class="form-input">
<input autocomplete="off" type="password" name="passwordConfirmation" id="account-settings-password-confirmation" placeholder="New password&hellip; (repeat)" value=""/>
</div>
</div>
<% } %>
<div class="form-row">
<label class="form-label" for="account-settings-password-confirmation"></label>
<div class="form-input">
<input autocomplete="off" type="password" name="passwordConfirmation" id="account-settings-password-confirmation" placeholder="New password&hellip; (repeat)" value=""/>
</div>
</div>
<% } %>
<% if (canBan) { %>
<div class="form-row">
<label class="form-label" for="account-settings-ban">Ban:</label>
<div class="form-input">
<input name="ban" type="checkbox" id="ban" <% print(user.banned ? 'checked="checked"' : '') %>>
<label for="ban">
Enabled
</label>
</div>
</div>
<% } %>
<% if (canBan) { %>
<div class="form-row">
<label class="form-label" for="account-settings-ban">Ban:</label>
<div class="form-input">
<input name="ban" type="checkbox" id="ban" <% print(user.banned ? 'checked="checked"' : '') %>>
<label for="ban">
Enabled
</label>
</div>
</div>
<% } %>
<% if (canChangeAccessRank) { %>
<div class="form-row">
<label class="form-label" for="account-settings-access-rank">Access rank:</label>
<div class="form-input">
<%
var accessRanks = {
anonymous: 'Anonymous',
restrictedUser: 'Restricted user',
regularUser: 'Regular user',
powerUser: 'Power user',
moderator: 'Moderator',
administrator: 'Administrator'
};
%>
<% _.each(accessRanks, function(v, k) { %>
<input name="access-rank" type="radio" value="<%= k %>" id="access-rank-<%= k %>" <% print(user.accessRank == k ? 'checked="checked"' : '') %>>
<label for="access-rank-<%= k %>">
<% print(user.accessRank == k ? v + ' (current)' : v) %>
</label>
<br/>
<% }) %>
</div>
</div>
<% } %>
<% if (canChangeAccessRank) { %>
<div class="form-row">
<label class="form-label" for="account-settings-access-rank">Access rank:</label>
<div class="form-input">
<%
var accessRanks = {
anonymous: 'Anonymous',
restrictedUser: 'Restricted user',
regularUser: 'Regular user',
powerUser: 'Power user',
moderator: 'Moderator',
administrator: 'Administrator'
};
%>
<% _.each(accessRanks, function(v, k) { %>
<input name="access-rank" type="radio" value="<%= k %>" id="access-rank-<%= k %>" <% print(user.accessRank == k ? 'checked="checked"' : '') %>>
<label for="access-rank-<%= k %>">
<% print(user.accessRank == k ? v + ' (current)' : v) %>
</label>
<br/>
<% }) %>
</div>
</div>
<% } %>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Update settings</button>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Update settings</button>
</div>
</div>
</form>

View File

@ -1,52 +1,94 @@
<div class="messages"></div>
<form class="form-wrapper browsing-settings">
<div class="form-row">
<label class="form-label">Safety:</label>
<div class="form-input">
<input <% print(settings.listPosts.safe ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-safety-safe" name="listSafePosts" value="safe"/>
<label for="browsing-settings-safety-safe">
Safe
</label>
<div class="form-row">
<label class="form-label">Safety:</label>
<div class="form-input">
<input <% print(settings.listPosts.safe ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-safety-safe" name="listSafePosts" value="safe"/>
<label for="browsing-settings-safety-safe">
Safe
</label>
<input <% print(settings.listPosts.sketchy ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-safety-sketchy" name="listSketchyPosts" value="sketchy"/>
<label for="browsing-settings-safety-sketchy">
Sketchy
</label>
<input <% print(settings.listPosts.sketchy ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-safety-sketchy" name="listSketchyPosts" value="sketchy"/>
<label for="browsing-settings-safety-sketchy">
Sketchy
</label>
<input <% print(settings.listPosts.unsafe ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-safety-unsafe" name="listUnsafePosts" value="unsafe"/>
<label for="browsing-settings-safety-unsafe">
Unsafe
</label>
</div>
</div>
<input <% print(settings.listPosts.unsafe ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-safety-unsafe" name="listUnsafePosts" value="unsafe"/>
<label for="browsing-settings-safety-unsafe">
Unsafe
</label>
</div>
</div>
<div class="form-row">
<label class="form-label" for="browsing-settings-endless-scroll">Endless scroll:</label>
<div class="form-input">
<input <% print(settings.endlessScroll ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-endless-scroll" name="endlessScroll"/>
<label for="browsing-settings-endless-scroll">
Enabled
</label>
</div>
</div>
<div class="form-row">
<label class="form-label" for="browsing-settings-endless-scroll">Endless scroll:</label>
<div class="form-input">
<input <% print(settings.endlessScroll ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-endless-scroll" name="endlessScroll"/>
<label for="browsing-settings-endless-scroll">
Enabled
</label>
</div>
</div>
<div class="form-row">
<label class="form-label" for="browsing-settings-hide-downvoted">Hide down-voted:</label>
<div class="form-input">
<input <% print(settings.hideDownvoted ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-hide-downvoted" name="hideDownvoted"/>
<label for="browsing-settings-hide-downvoted">
Enabled
</label>
</div>
</div>
<div class="form-row">
<label class="form-label" for="browsing-settings-hide-downvoted">Hide down-voted:</label>
<div class="form-input">
<input <% print(settings.hideDownvoted ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-hide-downvoted" name="hideDownvoted"/>
<label for="browsing-settings-hide-downvoted">
Enabled
</label>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Update settings</button>
</div>
</div>
<div class="form-row">
<label class="form-label" for="browsing-settings-keyboard-shortcuts">Keyboard shortcuts:</label>
<div class="form-input">
<input <% print(settings.keyboardShortcuts ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-keyboard-shortcuts" name="keyboardShortcuts"/>
<label for="browsing-settings-keyboard-shortcuts">
Enabled
</label>
</div>
</div>
<div class="form-row">
<label class="form-label">Default fit mode:</label>
<div class="form-input">
<input <% print(settings.fitMode === 'fit-width' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-width" name="fitMode" value="fit-width"/>
<label for="browsing-settings-fit-width">
Fit to window width
</label>
<br/>
<input <% print(settings.fitMode === 'fit-height' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-height" name="fitMode" value="fit-height"/>
<label for="browsing-settings-fit-height">
Fit to window height
</label>
<br/>
<input <% print(settings.fitMode === 'fit-both' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-both" name="fitMode" value="fit-both"/>
<label for="browsing-settings-fit-both">
Fit to both width and height
</label>
<br/>
<input <% print(settings.fitMode === 'original' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-original" name="fitMode" value="original"/>
<label for="browsing-settings-fit-original">
Show at original size
</label>
<br/>
<input <% print(settings.upscale ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-upscale" name="upscale" value="upscale"/>
<label for="browsing-settings-upscale">
Upscale small posts
</label>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Update settings</button>
</div>
</div>
</form>

View File

@ -1,17 +1,17 @@
<form class="comment-form">
<h1><%= title %></h1>
<h1><%= title %></h1>
<div class="preview"></div>
<div class="preview"></div>
<div class="form-row text">
<div class="input-wrapper">
<textarea name="text" cols="50" rows="3"><% if (typeof(text) !== 'undefined') { print(text) } %></textarea>
</div>
</div>
<div class="form-row text">
<div class="input-wrapper">
<textarea name="text" cols="50" rows="3"><% if (typeof(text) !== 'undefined') { print(text) } %></textarea>
</div>
</div>
<div class="form-row">
<button type="submit" name="sender" value="preview">Preview</button>&nbsp;
<button type="submit" name="sender" value="submit">Submit</button>
</div>
<div class="form-row">
<button type="submit" name="sender" value="preview">Preview</button>&nbsp;
<button type="submit" name="sender" value="submit">Submit</button>
</div>
</form>

View File

@ -1,66 +1,66 @@
<div class="comment">
<div class="avatar">
<% if (comment.user.name) { %>
<a href="#/user/<%= comment.user.name %>">
<% } %>
<div class="avatar">
<% if (comment.user.name && canViewUsers) { %>
<a href="#/user/<%= comment.user.name %>">
<% } %>
<img width="40" height="40" class="author-avatar"
src="/data/thumbnails/40x40/avatars/<%= comment.user.name || '!' %>"
alt="<%= comment.user.name || 'Anonymous user' %>"/>
<img width="40" height="40" class="author-avatar"
src="/data/thumbnails/40x40/avatars/<%= comment.user.name || '!' %>"
alt="<%= comment.user.name || 'Anonymous user' %>"/>
<% if (comment.user.name) { %>
</a>
<% } %>
</div>
<% if (comment.user.name && canViewUsers) { %>
</a>
<% } %>
</div>
<div class="body">
<div class="header">
<span class="nickname">
<% if (comment.user.name) { %>
<a href="#/user/<%= comment.user.name %>">
<% } %>
<div class="body">
<div class="header">
<span class="nickname">
<% if (comment.user.name && canViewUsers) { %>
<a href="#/user/<%= comment.user.name %>">
<% } %>
<%= comment.user.name || 'Anonymous user' %>
<%= comment.user.name || 'Anonymous user' %>
<% if (comment.user.name) { %>
</a>
<% } %>
</span>
<% if (comment.user.name && canViewUsers) { %>
</a>
<% } %>
</span>
<span class="date" title="<%= comment.creationTime %>">
<%= formatRelativeTime(comment.creationTime) %>
</span>
<span class="date" title="<%= util.formatAbsoluteTime(comment.creationTime) %>">
<%= util.formatRelativeTime(comment.creationTime) %>
</span>
<span class="score">
Score: <%= comment.score %>
</span>
<span class="score">
Score: <%= comment.score %>
</span>
<span class="ops"><!--
--><% if (canVote) { %><!--
--><a href="#" class="score-up <% print(comment.ownScore === 1 ? 'active' : '') %>"><!--
-->vote up<!--
--></a><!--
--><a href="#" class="score-down <% print(comment.ownScore === -1 ? 'active' : '') %>"><!--
-->vote down<!--
--></a><!--
--><% } %><!--
<span class="ops"><!--
--><% if (canVote) { %><!--
--><a href="#" class="score-up <% print(comment.ownScore === 1 ? 'active' : '') %>"><!--
-->vote up<!--
--></a><!--
--><a href="#" class="score-down <% print(comment.ownScore === -1 ? 'active' : '') %>"><!--
-->vote down<!--
--></a><!--
--><% } %><!--
--><% if (canEditComment) { %><!--
--><a href="#" class="edit"><!--
-->edit<!--
--></a><!--
--><% } %><!--
--><% if (canEditComment) { %><!--
--><a href="#" class="edit"><!--
-->edit<!--
--></a><!--
--><% } %><!--
--><% if (canDeleteComment) { %><!--
--><a href="#" class="delete"><!--
-->delete<!--
--></a><!--
--><% } %><!--
--></span>
</div>
--><% if (canDeleteComment) { %><!--
--><a href="#" class="delete"><!--
-->delete<!--
--></a><!--
--><% } %><!--
--></span>
</div>
<div class="content">
<%= formatMarkdown(comment.text) %>
</div>
</div>
<div class="content">
<%= util.formatMarkdown(comment.text) %>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
<% if (canListComments && comments.length) { %>
<div class="comments">
<h1>Comments</h1>
<ul class="comments">
</ul>
</div>
<% } %>
<% if (canAddComments) { %>
<div class="comment-add">
<%= commentFormTemplate({title: 'Add comment'}) %>
</div>
<% } %>

View File

@ -1,8 +1,8 @@
<div class="post-comment">
<div class="post">
<%= postTemplate({post: post, util: util}) %>
</div>
<div class="post">
<%= postTemplate({post: post, util: util, canViewPosts: canViewPosts}) %>
</div>
<div class="post-comments-target">
</div>
<div class="post-comments-target">
</div>
</div>

View File

@ -1,6 +1,6 @@
<div id="global-comment-list">
<div class="pagination-target">
<ul class="posts">
</ul>
</div>
<div class="pagination-target">
<ul class="posts">
</ul>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div id="history-wrapper">
<div class="pagination-target">
</div>
<div class="pagination-target">
</div>
</div>

View File

@ -1,274 +1,295 @@
<div id="help-view">
<ul class="tabs">
<li>
<a class="big-button" href="#/help/about">About</a>
</li>
<li>
<a class="big-button" href="#/help/keyboard">Keyboard</a>
</li>
<li>
<a class="big-button" href="#/help/search-syntax">Search syntax</a>
</li>
<li>
<a class="big-button" href="#/help/comments">Comments</a>
</li>
<li>
<a class="big-button" href="#/help/tos">Terms of service</a>
</li>
<li>
<a class="big-button" href="#/help/about">About</a>
</li>
<li>
<a class="big-button" href="#/help/keyboard">Keyboard</a>
</li>
<li>
<a class="big-button" href="#/help/search-syntax">Search syntax</a>
</li>
<li>
<a class="big-button" href="#/help/comments">Comments</a>
</li>
<li>
<a class="big-button" href="#/help/tos">Terms of service</a>
</li>
</ul>
<div data-tab="about">
<h1>About</h1>
<h1>About</h1>
<p>Szurubooru is an image board engine inspired by services such as
Danbooru, Gelbooru and Moebooru. Its name <a
href="http://sjp.pwn.pl/sjp/;2527372">has its roots in Polish language and
has onomatopeic meaning of scraping or scrubbing</a>. It is pronounced as
<em>shoorubooru</em>.</p>
<p>Szurubooru is an image board engine inspired by services such as
Danbooru, Gelbooru and Moebooru. Its name <a
href="http://sjp.pwn.pl/sjp/;2527372">has its roots in Polish language and
has onomatopeic meaning of scraping or scrubbing</a>. It is pronounced as
<em>shoorubooru</em>.</p>
<h1>Registration</h1>
<h1>Registration</h1>
<p>By default, szurubooru is shipped as an invite-only app. In other words,
in order to use the service, you need to register and have someone inside
accept your registration. The e-mail you enter during account creation is
only used to retrieve your Gravatar and activate your account. Only you can
see it (well, except the database staff&hellip; we won&rsquo;t spam your
mailbox anyway).</p>
<p>By default, szurubooru is shipped as an invite-only app. In other words,
in order to use the service, you need to register and have someone inside
accept your registration. The e-mail you enter during account creation is
only used to retrieve your Gravatar and activate your account. Only you can
see it (well, except the database staff&hellip; we won&rsquo;t spam your
mailbox anyway).</p>
<p>Oh, and you can delete your account at any time. Posts you uploaded will
stay, unless some angry admin removes them.</p>
<p>Oh, and you can delete your account at any time. Posts you uploaded will
stay, unless some angry admin removes them.</p>
</div>
<div data-tab="keyboard">
<h1>Keyboard shortcuts</h1>
<h1>Keyboard shortcuts</h1>
<p>You can use your keyboard to navigate around the site. There are a few
shortcuts:</p>
<p>You can use your keyboard to navigate around the site. There are a few
shortcuts:</p>
<table>
<thead>
<tr>
<th>Hotkey</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[Q]</code></td>
<td>Focus search field, if available</td>
</tr>
<table>
<thead>
<tr>
<th>Hotkey</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[Q]</code></td>
<td>Focus search field, if available</td>
</tr>
<tr>
<td><code>[A]</code> and <code>[D]</code></td>
<td>Go to newer/older page or post</td>
</tr>
<tr>
<td><code>[A]</code> and <code>[D]</code><br/><code>[Left]</code> and <code>[Right]</code> arrow keys</td>
<td>Go to newer/older page or post</td>
</tr>
<tr>
<td><code>[E]</code></td>
<td>Edit post</td>
</tr>
<tr>
<td><code>[F]</code></td>
<td>Cycle post fit mode</td>
</tr>
<tr>
<td><code>[P]</code></td>
<td>Focus first post in post list</td>
</tr>
</tbody>
</table>
<tr>
<td><code>[E]</code></td>
<td>Edit post</td>
</tr>
<p>Additionally, each item in top navigation can be accessed using feature
called &ldquo;access keys&rdquo;. Pressing underlined letter while holding
Shfit or Alt+Shift (depending on your browser) will go to the desired page
(most browsers) or focus the link (IE).</p>
<tr>
<td><code>[P]</code></td>
<td>Focus first post in post list</td>
</tr>
</tbody>
</table>
<p>Additionally, each item in top navigation can be accessed using feature
called &ldquo;access keys&rdquo;. Pressing underlined letter while holding
Shfit or Alt+Shift (depending on your browser) will go to the desired page
(most browsers) or focus the link (IE).</p>
</div>
<div data-tab="search-syntax">
<h1>Search syntax</h1>
<h1>Search syntax</h1>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<%
var table = [
{search: 'Haruhi', description: 'containing tag &ldquo;Haruhi&rdquo;'},
{search: '-Kyon', description: 'not containing tag &ldquo;Kyon&rdquo;'},
{search: 'uploader:David', description: 'uploaded by user David'},
{search: 'comment:David', description: 'commented by David'},
{search: 'fav:David', description: 'favorited by David'},
{search: 'fav_count:4', description: 'favorited by exactly four users'},
{search: 'fav_count:4,5', description: 'favorited by four or five users'},
{search: 'fav_count:4..', description: 'favorited by at least four users'},
{search: 'fav_count:..4', description: 'favorited by at most four users'},
{search: 'fav_count:4..6', description: 'favorited by at least four, but no more than six users'},
{search: 'comment_count:3', description: 'having exactly three comments'},
{search: 'score:4', description: 'having score of 4'},
{search: 'tag_count:7', description: 'tagged with exactly seven tags'},
{search: 'date:today', description: 'posted today'},
{search: 'date:yesterday', description: 'posted yesterday'},
{search: 'date:2000', description: 'posted in year 2000'},
{search: 'date:2000-01', description: 'posted in January, 2000'},
{search: 'date:2000-01-01', description: 'posted on January 1st, 2000'},
{search: 'id:1', description: 'having specific post ID'},
{search: 'name:<em>hash</em>', description: 'having specific post name (hash in full URLs)'},
{search: 'type:image', description: 'only image posts'},
{search: 'type:flash', description: 'only Flash posts'},
{search: 'type:youtube', description: 'only Youtube posts'},
{search: 'type:video', description: 'only video posts'},
{search: 'special:liked', description: 'posts liked by currently logged in user'},
{search: 'special:disliked', description: 'posts disliked by currently logged in user'},
{search: 'special:fav', description: 'posts added to favorites by currently logged in user'},
];
_.each(table, function(row) { %>
<tr>
<td><a href="#/posts/query=<%= row.search %>"><code><%= row.search %></code></a></td>
<td><%= row.description %></td>
</tr>
<% }) %>
</tbody>
</table>
<tbody>
<%
var table = [
{search: 'Haruhi', description: 'containing tag &ldquo;Haruhi&rdquo;'},
{search: '-Kyon', description: 'not containing tag &ldquo;Kyon&rdquo;'},
{search: 'uploader:David', description: 'uploaded by user David'},
{search: 'comment:David', description: 'commented by David'},
{search: 'fav:David', description: 'favorited by David'},
{search: 'fav_count:4', description: 'favorited by exactly four users'},
{search: 'fav_count:4,5', description: 'favorited by four or five users'},
{search: 'fav_count:4..', description: 'favorited by at least four users'},
{search: 'fav_count:..4', description: 'favorited by at most four users'},
{search: 'fav_count:4..6', description: 'favorited by at least four, but no more than six users'},
{search: 'comment_count:3', description: 'having exactly three comments'},
{search: 'score:4', description: 'having score of 4'},
{search: 'tag_count:7', description: 'tagged with exactly seven tags'},
{search: 'note_count:1..', description: 'having at least one post note'},
{search: 'feature_count:1..', description: 'having been featured at least once'},
{search: 'date:today', description: 'posted today'},
{search: 'date:yesterday', description: 'posted yesterday'},
{search: 'date:2000', description: 'posted in year 2000'},
{search: 'date:2000-01', description: 'posted in January, 2000'},
{search: 'date:2000-01-01', description: 'posted on January 1st, 2000'},
{search: 'id:1', description: 'having specific post ID'},
{search: 'name:<em>hash</em>', description: 'having specific post name (hash in full URLs)'},
{search: 'file_size:100..', description: 'having at least 100 bytes'},
{search: 'image_width:100..', description: 'being at least 100 pixels wide'},
{search: 'image_height:100..', description: 'being at least 100 pixels tall'},
{search: 'image_area:10000..', description: 'having at least 10000 pixels'},
{search: 'type:image', description: 'only image posts'},
{search: 'type:flash', description: 'only Flash posts'},
{search: 'type:youtube', description: 'only Youtube posts'},
{search: 'type:video', description: 'only video posts'},
{search: 'special:liked', description: 'posts liked by currently logged in user'},
{search: 'special:disliked', description: 'posts disliked by currently logged in user'},
{search: 'special:fav', description: 'posts added to favorites by currently logged in user'},
{search: 'special:tumbleweed', description: 'posts with score of 0, without comments and without favorites'},
];
_.each(table, function(row) { %>
<tr>
<td><a href="#/posts/query=<%= row.search %>"><code><%= row.search %></code></a></td>
<td><%= row.description %></td>
</tr>
<% }) %>
</tbody>
</table>
<p>Most of the commands support ranged and composites values, e.g.
<code>id:<em>number</em></code> operator supports respectively <a
href="#/posts/query=id:5..7"><code>id:5..7</code></a> and <a
href="#/posts/query=id:5,10,15"><code>id:5,10,15</code></a>. You can
combine tags and negate any of them for interesting results. <a
href="#/posts/query=sea -fav_count:..8 type:flash
uploader:Pirate"><code>sea -fav_count:8.. type:swf
uploader:Pirate</code></a> will show you flash files tagged as sea, that
were liked by seven people at most, uploaded by user Pirate.</p>
<p>Most of the commands support ranged and composites values, e.g.
<code>id:<em>number</em></code> operator supports respectively <a
href="#/posts/query=id:5..7"><code>id:5..7</code></a> and <a
href="#/posts/query=id:5,10,15"><code>id:5,10,15</code></a>. You can
combine tags and negate any of them for interesting results. <a
href="#/posts/query=sea -fav_count:..8 type:flash
uploader:Pirate"><code>sea -fav_count:8.. type:swf
uploader:Pirate</code></a> will show you flash files tagged as sea, that
were liked by seven people at most, uploaded by user Pirate.</p>
<p>All of the above can be sorted using additional tag in form of
<code>order:<em>keyword</em></code>:</p>
<p>All of the above can be sorted using additional tag in form of
<code>order:<em>keyword</em></code>:</p>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<%
var table = [
{search: 'order:random', description: 'as random as it can get'},
{search: 'order:id', description: 'highest to lowest post ID (default browse view)'},
{search: 'order:date', description: 'newest to oldest (pretty much same as above)'},
{search: '-order:date', description: 'oldest to newest'},
{search: 'order:date,asc', description: 'oldest to newest (ascending order, default = descending)'},
{search: 'order:score', description: 'highest scored'},
{search: 'order:file_size', description: 'largest files first'},
{search: 'order:tag_count', description: 'with most tags'},
{search: 'order:fav_count', description: 'loved by most'},
{search: 'order:comment_count', description: 'most commented first'},
{search: 'order:fav_date', description: 'recently added to favorites'},
{search: 'order:comment_date', description: 'recently commented'},
{search: 'order:feature_date', description: 'recently featured'},
];
_.each(table, function(row) { %>
<tr>
<td><a href="#/posts/query=<%= row.search %>"><code><%= row.search %></code></a></td>
<td><%= row.description %></td>
</tr>
<% }) %>
</tbody>
</table>
<tbody>
<%
var table = [
{search: 'order:random', description: 'as random as it can get'},
{search: 'order:id', description: 'highest to lowest post ID (default browse view)'},
{search: 'order:creation_date', description: 'newest to oldest (pretty much same as above)'},
{search: '-order:creation_date', description: 'oldest to newest'},
{search: 'order:creation_date,asc', description: 'oldest to newest (ascending order, default = descending)'},
{search: 'order:edit_date', description: 'like <code>creation_date</code>, only looks at last edit time'},
{search: 'order:score', description: 'highest scored'},
{search: 'order:file_size', description: 'largest files first'},
{search: 'order:image_width', description: 'widest images first'},
{search: 'order:image_height', description: 'tallest images first'},
{search: 'order:image_area', description: 'largest images first'},
{search: 'order:tag_count', description: 'with most tags'},
{search: 'order:fav_count', description: 'loved by most'},
{search: 'order:comment_count', description: 'most commented first'},
{search: 'order:fav_date', description: 'recently added to favorites'},
{search: 'order:comment_date', description: 'recently commented'},
{search: 'order:feature_date', description: 'recently featured'},
{search: 'order:feature_count', description: 'most often featured'},
];
_.each(table, function(row) { %>
<tr>
<td><a href="#/posts/query=<%= row.search %>"><code><%= row.search %></code></a></td>
<td><%= row.description %></td>
</tr>
<% }) %>
</tbody>
</table>
<p>As shown with <a
href="#/posts/query=-order:date"><code>-order:date</code></a>, any of them
can be reversed in the same way as negating other tags: by placing a dash
before the tag.</p>
<p>As shown with <a
href="#/posts/query=-order:creation_date"><code>-order:creation_date</code></a>,
any of them can be reversed in the same way as negating other tags: by
placing a dash before the tag.</p>
</div>
<div data-tab="comments">
<h1>Comments</h1>
<p>Comments support Markdown syntax, extended by some handy tags:</p>
<h1>Comments</h1>
<p>Comments support Markdown syntax, extended by some handy tags:</p>
<table>
<tbody>
<tr>
<td><code>@426</code></td>
<td>links to post number 426</td>
</tr>
<tr>
<td><code>#Dragon_Ball</code></td>
<td>links to tag &ldquo;Dragon_Ball&rdquo;</td>
</tr>
<tr>
<td><code>+Pirate</code></td>
<td>links to user &ldquo;Pirate&rdquo;</td>
</tr>
<tr>
<td><code>~~new~~</code></td>
<td>adds strike-through</td>
</tr>
<tr>
<td><code>[spoiler]Lelouch survives[/spoiler]</td>
<td>marks text as spoiler and hides it</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td><code>@426</code></td>
<td>links to post number 426</td>
</tr>
<tr>
<td><code>#Dragon_Ball</code></td>
<td>links to tag &ldquo;Dragon_Ball&rdquo;</td>
</tr>
<tr>
<td><code>+Pirate</code></td>
<td>links to user &ldquo;Pirate&rdquo;</td>
</tr>
<tr>
<td><code>~~new~~</code></td>
<td>adds strike-through</td>
</tr>
<tr>
<td><code>[spoiler]Lelouch survives[/spoiler]</td>
<td>marks text as spoiler and hides it</td>
</tr>
<tr>
<td><code>[sjis](´・ω・`)[/sjis]</td>
<td>adds SJIS art</td>
</tr>
</tbody>
</table>
</div>
<div data-tab="tos">
<h1>Terms of service</h1>
<h1>Terms of service</h1>
<p>By accessing <%= title %> (&ldquo;Site&rdquo;) you agree to the
following Terms of Service. If you do not agree to these terms, then please
do not access the Site.</p>
<p>By accessing <%= title %> (&ldquo;Site&rdquo;) you agree to the
following Terms of Service. If you do not agree to these terms, then please
do not access the Site.</p>
<ul>
<li>The Site is presented to you AS IS, without any warranty, express
or implied. You will not hold the Site or its staff members liable for
damages caused by the use of the site.</li>
<li>The Site reserves the right to delete or modify your account, or
any content you have posted to the site.</li>
<li>The Site reserves the right to change these Terms of Service
without prior notice.</li>
<li>If you are a minor, then you will not use the Site.</li>
<li>You are using the Site only for personal use.</li>
<li>You will not spam, troll or offend anyone.</li>
<li>You accept that the Site is not liable for any content that you may stumble upon.</li>
</ul>
<ul>
<li>The Site is presented to you AS IS, without any warranty, express
or implied. You will not hold the Site or its staff members liable for
damages caused by the use of the site.</li>
<li>The Site reserves the right to delete or modify your account, or
any content you have posted to the site.</li>
<li>The Site reserves the right to change these Terms of Service
without prior notice.</li>
<li>If you are a minor, then you will not use the Site.</li>
<li>You are using the Site only for personal use.</li>
<li>You will not spam, troll or offend anyone.</li>
<li>You accept that the Site is not liable for any content that you may stumble upon.</li>
</ul>
<p><strong>Prohibited content</strong></p>
<p><strong>Prohibited content</strong></p>
<ul>
<li>Child pornography: any photograph or photorealistic drawing or
movie that depicts children in a sexual manner. This includes nudity,
explicit sex, implied sex, or sexually persuasive positions.</li>
<ul>
<li>Child pornography: any photograph or photorealistic drawing or
movie that depicts children in a sexual manner. This includes nudity,
explicit sex, implied sex, or sexually persuasive positions.</li>
<li>Bestiality: any photograph or photorealistic drawing or movie that
depicts humans having sex (either explicit or implied) with other
non-human animals.</li>
<li>Bestiality: any photograph or photorealistic drawing or movie that
depicts humans having sex (either explicit or implied) with other
non-human animals.</li>
<li>Any depiction of extreme mutilation, extreme bodily distension,
feces.</li>
<li>Any depiction of extreme mutilation, extreme bodily distension,
feces.</li>
<li>Personal images: any image that is suspected to be uploaded for
personal use. This includes, but is not limited to, avatars and forum
signatures.</li>
</ul>
<li>Personal images: any image that is suspected to be uploaded for
personal use. This includes, but is not limited to, avatars and forum
signatures.</li>
</ul>
<h1>Privacy policy</h1>
<h1>Privacy policy</h1>
<p>The Site will not disclose the IP address or email address of any user
except to the staff.</p>
<p>The Site will not disclose the IP address or email address of any user
except to the staff.</p>
Posts, comments, favorites, ratings and other actions linked to your
account will be stored in the Site&rsquo;s database. The &ldquo;Upload
anonymously&rdquo; option allows you to post content without linking it to
your account&nbsp;&ndash; meaning your nickname will not be stored in the
database nor shown in the &ldquo;Uploader&rdquo; field.</p>
Posts, comments, favorites, ratings and other actions linked to your
account will be stored in the Site&rsquo;s database. The &ldquo;Upload
anonymously&rdquo; option allows you to post content without linking it to
your account&nbsp;&ndash; meaning your nickname will not be stored in the
database nor shown in the &ldquo;Uploader&rdquo; field.</p>
<p>Cookies are used to store your session data in order to keep you logged
in and personalize your web experience.</p>
<p>Cookies are used to store your session data in order to keep you logged
in and personalize your web experience.</p>
</div>
</div>

View File

@ -1,80 +1,82 @@
<%
var reprValue = function(value) {
if (typeof(value) === 'string' || value instanceof String) {
return value;
}
return JSON.stringify(value);
if (typeof(value) === 'string' || value instanceof String) {
return value;
}
return JSON.stringify(value);
};
var showDifference = function(className, difference) {
_.each(difference, function(value, key) {
if (!Array.isArray(value)) {
value = [value];
}
_.each(value, function(v) {
%><li class="<%= className %> difference-<%= key %>"><%= key + ':' + reprValue(v) %></li><%
});
});
};
%>
<table class="history">
<tbody>
<% _.each(history, function( historyEntry) { %>
<tr>
<td class="time">
<%= formatRelativeTime(historyEntry.time) %>
</td>
<tbody>
<% _.each(history, function( historyEntry) { %>
<tr>
<td class="time" title="<%= util.formatAbsoluteTime(historyEntry.time) %>">
<%= util.formatRelativeTime(historyEntry.time) %>
</td>
<td class="user">
<% var userName = historyEntry.user && historyEntry.user.name || '' %>
<td class="user">
<% var userName = historyEntry.user && historyEntry.user.name || '' %>
<% if (userName) { %>
<a href="#/user/<%= userName %>">
<% } %>
<% if (userName) { %>
<a href="#/user/<%= userName %>">
<% } %>
<img width="20" height="20" class="author-avatar"
src="/data/thumbnails/20x20/avatars/<%= userName || '!' %>"
alt="<%= userName || 'Anonymous user' %>"/>
<img width="20" height="20" class="author-avatar"
src="/data/thumbnails/20x20/avatars/<%= userName || '!' %>"
alt="<%= userName || 'Anonymous user' %>"/>
<%= userName || 'Anonymous user' %>
<%= userName || 'Anonymous user' %>
<% if (userName) { %>
</a>
<% } %>
</td>
<% if (userName) { %>
</a>
<% } %>
</td>
<td class="subject">
<% if (historyEntry.type === 0) { %>
<a href="#/post/<%= historyEntry.primaryKey %>">
@<%= historyEntry.primaryKey %>
</a>
<% } else if (historyEntry.type === 1) { %>
<a href="#/tag/<%= historyEntry.data.name %>">
#<%= historyEntry.data.name %>
</a>
<% } else { %>
?
<% } %>
</td>
<td class="subject">
<% if (historyEntry.type === 0) { %>
<a href="#/post/<%= historyEntry.primaryKey %>">
@<%= historyEntry.primaryKey %>
</a>
<% } else if (historyEntry.type === 1) { %>
<a href="#/tag/<%= historyEntry.data.name %>">
#<%= historyEntry.data.name %>
</a>
<% } else { %>
?
<% } %>
</td>
<td class="difference">
<% if (historyEntry.operation == 2) { %>
deleted
<% } else { %>
<% if (historyEntry.operation == 0) { %>
added
<% } else { %>
changed
<% } %>
<td class="difference">
<% if (historyEntry.operation == 2) { %>
deleted
<% } else { %>
<% if (historyEntry.operation == 0) { %>
added
<% } else { %>
changed
<% } %>
<% if (historyEntry.dataDifference) { %>
<ul><!--
--><% _.each(historyEntry.dataDifference['+'], function (difference) { %><!--
--><li class="addition difference-<%= difference[0] %>"><!--
--><%= difference[0] + ':' + reprValue(difference[1]) %><!--
--></li><!--
--><% }) %><!--
--><% _.each(historyEntry.dataDifference['-'], function (difference) { %><!--
--><li class="removal difference-<%= difference[0] %>"><!--
--><%= difference[0] + ':' + reprValue(difference[1]) %><!--
--></li><!--
--><% }) %><!--
--></ul>
<% } %>
<% } %>
</td>
</tr>
<% }) %>
</tbody>
<% if (historyEntry.dataDifference) { %>
<ul><!--
--><% showDifference('addition', historyEntry.dataDifference['+']) %><!--
--><% showDifference('removal', historyEntry.dataDifference['-']) %><!--
--></ul>
<% } %>
<% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>

View File

@ -1,64 +1,69 @@
<% function showUser(name) { %>
<% var showLink = typeof(canViewUsers) !== 'undefined' && canViewUsers && name %>
<% if (showLink) { %>
<a href="#/user/<%= name %>">
<% } %>
<img width="25" height="25" class="author-avatar"
src="/data/thumbnails/25x25/avatars/<%= name || '!' %>"
alt="<%= name || 'Anonymous user' %>"/>
<%= name || 'Anonymous user' %>
<% if (showLink) { %>
</a>
<% } %>
<% } %>
<div id="home">
<h1><%= title %></h1>
<p>
<small>Serving <%= globals.postCount || 0 %> posts (<%= formatFileSize(globals.postSize || 0) %>)</small>
</p>
<h1><%= title %></h1>
<p class="subheader">
Serving <%= globals.postCount || 0 %> posts (<%= util.formatFileSize(globals.postSize || 0) %>)
</p>
<% if (post && post.id) { %>
<div class="post">
<div id="post-content-target">
</div>
<% if (post && post.id) { %>
<div class="post" style="width: <%= post.imageWidth || 800 %>px">
<div id="post-content-target">
</div>
<div class="post-footer">
<div class="post-footer">
<small class="left">
<% var showLink = canViewPosts %>
<span class="left">
<% var showLink = canViewPosts %>
<% if (showLink) { %>
<a href="#/post/<%= post.id %>">
<% } %>
<% if (showLink) { %>
<a href="#/post/<%= post.id %>">
<% } %>
<%= post.idMarkdown %>
<%= post.idMarkdown %>
<% if (showLink) { %>
</a>
<% } %>
<% if (showLink) { %>
</a>
<% } %>
uploaded
<%= formatRelativeTime(post.uploadTime) %>
</small>
uploaded
<%= util.formatRelativeTime(post.creationTime) %>
by
<% showUser(post.user.name) %>
</span>
<small class="right">
featured
<%= formatRelativeTime(post.lastFeatureTime) %>
by
<span class="right">
featured
<%= util.formatRelativeTime(post.lastFeatureTime) %>
by
<% showUser(user.name) %>
</span>
<% var showLink = canViewUsers && user.name %>
</div>
</div>
<% } %>
<% if (showLink) { %>
<a href="#/user/<%= user.name %>">
<% } %>
<img width="25" height="25" class="author-avatar"
src="/data/thumbnails/25x25/avatars/<%= user.name || '!' %>"
alt="<%= user.name || 'Anonymous user' %>"/>
<%= user.name || 'Anonymous user' %>
<% if (showLink) { %>
</a>
<% } %>
</small>
</div>
</div>
<% } %>
<p>
<small class="version">
Version: <a href="//github.com/rr-/szurubooru/commits/master"><%= version %></a> (built <%= formatRelativeTime(buildTime) %>)
|
<a href="#/history">Recent tag and post edits</a>
</small>
</p>
<p>
<small class="version">
Version: <a href="//github.com/rr-/szurubooru/commits/master"><%= version %></a> (built <%= util.formatRelativeTime(buildTime) %>)
|
<a href="#/history">Recent tag and post edits</a>
</small>
</p>
</div>

View File

@ -1,45 +1,45 @@
<div id="login-form">
<p>
If you don't have an account yet,<br/>
<a href="#/register">click here</a> to create a new one.
</p>
<p>
If you don't have an account yet,<br/>
<a href="#/register">click here</a> to create a new one.
</p>
<div class="messages"></div>
<div class="messages"></div>
<form class="form-wrapper">
<div class="form-row">
<label class="form-label" for="login-user">User name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="user" id="login-user"/>
</div>
</div>
<form class="form-wrapper">
<div class="form-row">
<label class="form-label" for="login-user">User name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="user" id="login-user"/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="login-password">Password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password" id="login-password"/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="login-password">Password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password" id="login-password"/>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Log in</button>
&nbsp;
<input type="checkbox" name="remember" id="login-remember"/>
<label for="login-remember">
Remember me
</label>
</div>
</div>
</form>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Log in</button>
&nbsp;
<input type="checkbox" name="remember" id="login-remember"/>
<label for="login-remember">
Remember me
</label>
</div>
</div>
</form>
<div class="help">
<p>Problems logging in?</p>
<ul>
<li><a href="#/password-reset">I don't remember my password</a></li>
<li><a href="#/activate">I haven't received activation e-mail</a></li>
<li><a href="#/register">I don't have an account</a></li>
</ul>
</div>
<div class="help">
<p>Problems logging in?</p>
<ul>
<li><a href="#/password-reset">I don't remember my password</a></li>
<li><a href="#/activate">I haven't received activation e-mail</a></li>
<li><a href="#/register">I don't have an account</a></li>
</ul>
</div>
</div>

View File

@ -2,4 +2,6 @@
</div>
<ul class="page-list">
<li class="prev"><a href="#">Prev</a></li>
<li class="next"><a href="#">Next</a></li>
</ul>

View File

@ -1,13 +0,0 @@
<% if (canListComments && comments.length) { %>
<div class="comments">
<h1>Comments</h1>
<ul class="comments">
</ul>
</div>
<% } %>
<% if (canAddComments) { %>
<div class="comment-add">
<%= commentFormTemplate({title: 'Add comment'}) %>
</div>
<% } %>

View File

@ -1,43 +1,60 @@
<% var postContentUrl = '/data/posts/' + post.name + '?x=' + Math.random() /* reset gif animations */ %>
<%
var postContentUrl = '/data/posts/' + post.name;
var width;
var height;
if (post.contentType === 'image' || post.contentType === 'animation' || post.contentType === 'flash') {
width = post.imageWidth;
height = post.imageHeight;
}
if (!width) { width = 800; }
if (!height) { height = 450; }
%>
<div class="post-content post-type-<%= post.contentType %>">
<div class="post-notes-target">
</div>
<div class="post-notes-target">
</div>
<% if (post.contentType === 'image') { %>
<div
class="object-wrapper"
data-width="<%= width %>"
data-height="<%= height %>"
style="max-width: <%= width %>px">
<div class="image-wrapper" style="width: <%= post.imageWidth %>px">
<img alt="<%= post.name %>" src="<%= postContentUrl %>"/>
<div style="padding-top: calc(100% * <%= post.imageHeight %> / <%= post.imageWidth %>)"></div>
</div>
<% if (post.contentType === 'image' || post.contentType === 'animation') { %>
<% } else if (post.contentType === 'youtube') { %>
<img alt="<%= post.name %>" src="<%= postContentUrl %>"/>
<iframe src="//www.youtube.com/embed/<%= post.contentChecksum %>?wmode=opaque" allowfullscreen></iframe>
<% } else if (post.contentType === 'youtube') { %>
<% } else if (post.contentType === 'flash') { %>
<iframe src="//www.youtube.com/embed/<%= post.contentChecksum %>?wmode=opaque" allowfullscreen></iframe>
<object
type="<%= post.contentMimeType %>"
width="<%= post.imageWidth %>"
height="<%= post.imageHeight %>"
data="<%= postContentUrl %>">
<param name="wmode" value="opaque"/>
<param name="movie" value="<%= postContentUrl %>"/>
</object>
<% } else if (post.contentType === 'flash') { %>
<% } else if (post.contentType === 'video') { %>
<% if (post.flags.loop) { %>
<video id="video" controls loop="loop">
<% } else { %>
<video id="video" controls>
<% } %>
<object
type="<%= post.contentMimeType %>"
width="<%= width %>"
height="<%= height %>"
data="<%= postContentUrl %>">
<param name="wmode" value="opaque"/>
<param name="movie" value="<%= postContentUrl %>"/>
</object>
<source type="<%= post.contentMimeType %>" src="<%= postContentUrl %>"/>
<% } else if (post.contentType === 'video') { %>
Your browser doesn't support HTML5 videos.
</video>
<% if (post.flags.loop) { %>
<video id="video" controls loop="loop">
<% } else { %>
<video id="video" controls>
<% } %>
<% } else { console.log(new Error('Unknown post type')) } %>
<source type="<%= post.contentMimeType %>" src="<%= postContentUrl %>"/>
Your browser doesn't support HTML5 videos.
</video>
<% } else { console.log(new Error('Unknown post type')) } %>
<div class="padding-fix" style="padding-bottom: calc(100% * <%= height %> / <%= width %>)"></div>
</div>
</div>

View File

@ -1,87 +1,94 @@
<form class="form-wrapper post-edit">
<% if (privileges.canChangeSafety) { %>
<div class="form-row">
<label class="form-label">Safety:</label>
<div class="form-input">
<input type="radio" id="post-safety-safe" name="safety" value="safe" <%= post.safety === 'safe' ? 'checked="checked"' : '' %>/>
<label for="post-safety-safe">
Safe
</label>
<% if (privileges.canChangeSafety) { %>
<div class="form-row">
<label class="form-label">Safety:</label>
<div class="form-input">
<input type="radio" id="post-safety-safe" name="safety" value="safe" <%= post.safety === 'safe' ? 'checked="checked"' : '' %>/>
<label for="post-safety-safe">
Safe
</label>
<input type="radio" id="post-safety-sketchy" name="safety" value="sketchy" <%= post.safety === 'sketchy' ? 'checked="checked"' : '' %>/>
<label for="post-safety-sketchy">
Sketchy
</label>
<input type="radio" id="post-safety-sketchy" name="safety" value="sketchy" <%= post.safety === 'sketchy' ? 'checked="checked"' : '' %>/>
<label for="post-safety-sketchy">
Sketchy
</label>
<input type="radio" id="post-safety-unsafe" name="safety" value="unsafe" <%= post.safety === 'unsafe' ? 'checked="checked"' : '' %>/>
<label for="post-safety-unsafe">
Unsafe
</label>
</div>
</div>
<% } %>
<input type="radio" id="post-safety-unsafe" name="safety" value="unsafe" <%= post.safety === 'unsafe' ? 'checked="checked"' : '' %>/>
<label for="post-safety-unsafe">
Unsafe
</label>
</div>
</div>
<% } %>
<% if (privileges.canChangeTags) { %>
<div class="form-row">
<label class="form-label" for="post-tags">Tags:</label>
<div class="form-input">
<input type="text" name="tags" id="post-tags" placeholder="Enter some tags&hellip;" value="<%= _.pluck(post.tags, 'name').join(' ') %>"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeTags) { %>
<div class="form-row">
<label class="form-label" for="post-tags">Tags:</label>
<div class="form-input">
<input type="text" name="tags" id="post-tags" placeholder="Enter some tags&hellip;" value="<%= _.pluck(post.tags, 'name').join(' ') %>"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeSource) { %>
<div class="form-row">
<label class="form-label" for="post-source">Source:</label>
<div class="form-input">
<input maxlength="200" type="text" name="source" id="post-source" placeholder="Where did you get this? (optional)" value="<%= post.source %>"/>
</div>
</div>
<% } %>
<div class="form-row advanced-trigger">
<label></label>
<div class="form-input">
<a href="#">Advanced&hellip;</a>
</div>
</div>
<% if (privileges.canChangeRelations) { %>
<div class="form-row">
<label class="form-label" for="post-relations">Relations:</label>
<div class="form-input">
<input maxlength="200" type="text" name="relations" id="post-relations" placeholder="Post ids, separated with space" value="<%= _.pluck(post.relations, 'id').join(' ') %>"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeSource) { %>
<div class="form-row advanced">
<label class="form-label" for="post-source">Source:</label>
<div class="form-input">
<input maxlength="200" type="text" name="source" id="post-source" placeholder="Where did you get this? (optional)" value="<%= post.source %>"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeFlags && post.contentType === 'video') { %>
<div class="form-row">
<label class="form-label">Loop:</label>
<div class="form-input">
<input type="checkbox" id="post-loop" name="loop" value="loop" <%= post.flags.loop ? 'checked="checked"' : '' %>/>
<label for="post-loop">
Automatically repeat video after playback
</label>
</div>
</div>
<% } %>
<% if (privileges.canChangeRelations) { %>
<div class="form-row advanced">
<label class="form-label" for="post-relations">Relations:</label>
<div class="form-input">
<input maxlength="200" type="text" name="relations" id="post-relations" placeholder="Post ids, separated with space" value="<%= _.pluck(post.relations, 'id').join(' ') %>"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeContent) { %>
<div class="form-row">
<label class="form-label" for="post-content">Content:</label>
<div class="form-input">
<input type="file" id="post-content" name="content"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeFlags && post.contentType === 'video') { %>
<div class="form-row advanced">
<label class="form-label">Loop:</label>
<div class="form-input">
<input type="checkbox" id="post-loop" name="loop" value="loop" <%= post.flags.loop ? 'checked="checked"' : '' %>/>
<label for="post-loop">
Automatically repeat video after playback
</label>
</div>
</div>
<% } %>
<% if (privileges.canChangeThumbnail) { %>
<div class="form-row">
<label class="form-label" for="post-thumbnail">Thumbnail:</label>
<div class="form-input">
<input type="file" id="post-thumbnail" name="thumbnail"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeContent) { %>
<div class="form-row advanced">
<label class="form-label" for="post-content">Content:</label>
<div class="form-input">
<input type="file" id="post-content" name="content"/>
</div>
</div>
<% } %>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Update</button>
</div>
</div>
<% if (privileges.canChangeThumbnail) { %>
<div class="form-row advanced">
<label class="form-label" for="post-thumbnail">Thumbnail:</label>
<div class="form-input">
<input type="file" id="post-thumbnail" name="thumbnail"/>
</div>
</div>
<% } %>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Update</button>
</div>
</div>
</form>

View File

@ -1,39 +1,49 @@
<div class="post-small post-type-<%= post.contentType %> ">
<a class="link"
href="<%= util.appendComplexRouteParam('#/post/' + post.id, typeof(query) !== 'undefined' ? query : {}) %>"
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>">
<img width="160" height="160" class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="<%= post.idMarkdown %>"/>
<% if (canViewPosts) { %>
<a class="link"
href="<%= util.appendComplexRouteParam('#/post/' + post.id, util.simplifySearchQuery(typeof(query) !== 'undefined' ? query : {})) %>"
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>">
<% } else { %>
<span class="link">
<% } %>
<% if (post.favoriteCount || post.score || post.commentCount) { %>
<div class="info">
<ul>
<% if (post.favoriteCount) { %>
<li>
<i class="fa fa-heart"></i>
<%= post.favoriteCount %>
</li>
<% } %>
<img width="160" height="160" class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="<%= post.idMarkdown %>"/>
<% if (post.score) { %>
<li>
<i class="fa fa-thumbs-up"></i>
<%= post.score %>
</li>
<% } %>
<% if (post.favoriteCount || post.score || post.commentCount) { %>
<div class="info">
<ul>
<% if (post.favoriteCount) { %>
<li>
<i class="fa fa-heart"></i>
<%= post.favoriteCount %>
</li>
<% } %>
<% if (post.commentCount) { %>
<li>
<i class="fa fa-comments"></i>
<%= post.commentCount %>
</li>
<% } %>
</ul>
</div>
<% } %>
</a>
<% if (post.score) { %>
<li>
<i class="fa fa-thumbs-up"></i>
<%= post.score %>
</li>
<% } %>
<div class="action">
<button>Action</button>
</div>
<% if (post.commentCount) { %>
<li>
<i class="fa fa-comments"></i>
<%= post.commentCount %>
</li>
<% } %>
</ul>
</div>
<% } %>
<% if (canViewPosts) { %>
</a>
<% } else { %>
</span>
<% } %>
<div class="action">
<button>Action</button>
</div>
</div>

View File

@ -1,32 +1,32 @@
<div class="post-list">
<form class="search">
<input type="text" name="query" placeholder="Search query..."/>
<button type="submit" name="search">Search</button>
<form class="search">
<input type="text" name="query" placeholder="Search query..."/>
<button type="submit" name="search">Search</button>
<ul class="safety">
<li>
<button class="safety-safe <%= browsingSettings.listPosts.safe ? '' : 'disabled' %>">&nbsp;</button>
</li>
<li>
<button class="safety-sketchy <%= browsingSettings.listPosts.sketchy ? '' : 'disabled' %>">&nbsp;</button>
</li>
<li>
<button class="safety-unsafe <%= browsingSettings.listPosts.unsafe ? '' : 'disabled' %>">&nbsp;</button>
</li>
</ul>
<ul class="safety">
<li>
<button class="safety-safe <%= browsingSettings.listPosts.safe ? '' : 'disabled' %>">&nbsp;</button>
</li>
<li>
<button class="safety-sketchy <%= browsingSettings.listPosts.sketchy ? '' : 'disabled' %>">&nbsp;</button>
</li>
<li>
<button class="safety-unsafe <%= browsingSettings.listPosts.unsafe ? '' : 'disabled' %>">&nbsp;</button>
</li>
</ul>
<% if (privileges.canMassTag) { %>
<div class="mass-tag-wrapper">
<p class="mass-tag-info">Tagging with <span class="mass-tag"><%= massTag %></span></p><!--
--><button name="mass-tag">Mass tag</button>
</div>
<% } %>
</form>
<% if (privileges.canMassTag) { %>
<div class="mass-tag-wrapper">
<p class="mass-tag-info">Tagging with <span class="mass-tag"><%= massTag %></span></p><!--
--><button name="mass-tag">Mass tag</button>
</div>
<% } %>
</form>
<div class="pagination-target">
<div class="wrapper">
<ul class="posts">
</ul>
</div>
</div>
<div class="pagination-target">
<div class="wrapper">
<ul class="posts">
</ul>
</div>
</div>
</div>

View File

@ -1,31 +1,31 @@
<div class="post-notes">
<% _.each(notes, function(note) { %>
<div class="post-note"
style="left: <%= note.left %>%;
top: <%= note.top %>%;
width: <%= note.width %>%;
height: <%= note.height %>%">
<% _.each(notes, function(note) { %>
<div tabindex="0" class="post-note"
style="left: <%= note.left %>%;
top: <%= note.top %>%;
width: <%= note.width %>%;
height: <%= note.height %>%">
<div class="text-wrapper">
<div class="text">
<%= formatMarkdown(note.text) %>
</div>
</div>
<div class="text-wrapper">
<div class="text">
<%= util.formatMarkdown(note.text) %>
</div>
</div>
</div>
<% }) %>
</div>
<% }) %>
</div>
<form class="post-note-edit">
<textarea></textarea>
<div class="actions"><!--
--><% if (privileges.canEditPostNotes) { %><!--
--><button type="submit" name="sender" value="save">Save</button><!--
--><button type="submit" name="sender" value="preview">Preview</button><!--
--><% } %><!--
--><button type="submit" name="sender" value="cancel">Cancel</button><!--
--><% if (privileges.canDeletePostNotes) { %><!--
--><button type="submit" name="sender" value="remove">Remove</button><!--
--><% } %><!--
--></div>
<textarea></textarea>
<div class="actions"><!--
--><% if (privileges.canEditPostNotes) { %><!--
--><button type="submit" name="sender" value="save">Save</button><!--
--><button type="submit" name="sender" value="preview">Preview</button><!--
--><% } %><!--
--><button type="submit" name="sender" value="cancel">Cancel</button><!--
--><% if (privileges.canDeletePostNotes) { %><!--
--><button type="submit" name="sender" value="remove">Remove</button><!--
--><% } %><!--
--></div>
</form>

View File

@ -1,141 +1,146 @@
<div id="post-upload-step1">
<input name="post-content" multiple type="file"/>
<input name="post-content" multiple type="file"/>
<div class="url-handler">
<div class="input-wrapper">
<input type="text" placeholder="Alternatively, paste an URL here." name="url"/>
</div>
<button type="submit">Add URL</button>
</div>
<div class="url-handler">
<div class="input-wrapper">
<input type="text" placeholder="Alternatively, paste an URL here." name="url"/>
</div>
<button type="submit">Add URL</button>
</div>
<div class="clear"></div>
<div class="clear"></div>
</div>
<div id="post-upload-step2">
<hr>
<hr>
<div class="hybrid-view">
<div class="hybrid-window">
<table>
<thead>
<tr>
<th class="checkbox">
<input id="post-upload-select-all" type="checkbox" name="select-all"/>
<label for="post-upload-select-all"></label>
</th>
<th class="thumbnail"></th>
<th class="tags">Tags</th>
<th class="safety">Safety</th>
</tr>
</thead>
<div class="hybrid-view">
<div class="hybrid-window">
<table>
<thead>
<tr>
<th class="checkbox">
<input id="post-upload-select-all" type="checkbox" name="select-all"/>
<label for="post-upload-select-all"></label>
</th>
<th class="thumbnail"></th>
<th class="tags">Tags</th>
<th class="safety">Safety</th>
</tr>
</thead>
<tbody>
</tbody>
<tbody>
</tbody>
<tfoot>
<tr class="template">
<td class="checkbox">
<input type="checkbox"/>
<label></label>
</td>
<td class="thumbnail">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
</td>
<td class="tags"></td>
<td class="safety"><div class="safety-template"></div></td>
</tr>
</tfoot>
</table>
<tfoot>
<tr class="template">
<td class="checkbox">
<input type="checkbox"/>
<label></label>
</td>
<td class="thumbnail">
<a href="#"/>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
</a>
</td>
<td class="tags"></td>
<td class="safety"><div class="safety-template"></div></td>
</tr>
</tfoot>
</table>
<ul class="operations"><!--
--><li>
<button class="post-table-op remove"><i class="fa fa-remove"></i> Remove</button>
</li><!--
--><li>
<button class="post-table-op move-up"><i class="fa fa-chevron-up"></i> Move up</button>
</li><!--
--><li>
<button class="post-table-op move-down"><i class="fa fa-chevron-down"></i> Move down</button>
</li><!--
--><li>
<button class="upload highlight" type="submit"><i class="fa fa-upload"></i> Submit</button>
</li><!--
--><li>
<button class="stop highlight-red" type="submit"><i class="fa fa-times-circle"></i> Stop</button>
</li><!--
--></ul>
<ul class="operations"><!--
--><li>
<button class="post-table-op remove"><i class="fa fa-remove"></i> Remove</button>
</li><!--
--><li>
<button class="post-table-op previous"><i class="fa fa-chevron-up"></i> Previous</button>
</li><!--
--><li>
<button class="post-table-op next"><i class="fa fa-chevron-down"></i> Next</button>
</li><!--
--><li>
<button class="post-table-op move-up"><i class="fa fa-arrow-up"></i> Move up</button>
</li><!--
--><li>
<button class="post-table-op move-down"><i class="fa fa-arrow-down"></i> Move down</button>
</li><!--
--><li>
<button class="upload highlight" type="submit"><i class="fa fa-upload"></i> Submit</button>
</li><!--
--><li>
<button class="stop highlight-red" type="submit"><i class="fa fa-times-circle"></i> Stop</button>
</li><!--
--></ul>
</div>
</div>
<div class="hybrid-window">
<div class="messages"></div>
<div class="hybrid-window">
<div class="messages"></div>
<div class="form-slider">
<div class="thumbnail">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
</div>
<div class="form-slider">
<div class="thumbnail">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
<a href="#" target="_blank">Open preview in a new tab</a>
</div>
<form class="form-wrapper">
<div class="form-row file-name">
<label class="form-label">File:</label>
<div class="form-input">
<strong>filename.jpg</strong>
</div>
</div>
<form class="form-wrapper">
<div class="form-row file-name">
<label class="form-label">File:</label>
<div class="form-input">
<strong>filename.jpg</strong>
</div>
</div>
<div class="form-row">
<label class="form-label">Safety:</label>
<div class="form-input">
<input type="radio" id="post-safety-safe" name="safety" value="safe"/>
<label for="post-safety-safe">
Safe
</label>
<div class="form-row">
<label class="form-label">Safety:</label>
<div class="form-input">
<input type="radio" id="post-safety-safe" name="safety" value="safe"/>
<label for="post-safety-safe">
Safe
</label>
<input type="radio" id="post-safety-sketchy" name="safety" value="sketchy"/>
<label for="post-safety-sketchy">
Sketchy
</label>
<input type="radio" id="post-safety-sketchy" name="safety" value="sketchy"/>
<label for="post-safety-sketchy">
Sketchy
</label>
<input type="radio" id="post-safety-unsafe" name="safety" value="unsafe"/>
<label for="post-safety-unsafe">
Unsafe
</label>
</div>
</div>
<input type="radio" id="post-safety-unsafe" name="safety" value="unsafe"/>
<label for="post-safety-unsafe">
Unsafe
</label>
</div>
</div>
<div class="form-row">
<label class="form-label" for="post-tags">Tags:</label>
<div class="form-input">
<input type="text" name="tags" id="post-tags" placeholder="Enter some tags&hellip;" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="post-tags">Tags:</label>
<div class="form-input">
<input type="text" name="tags" id="post-tags" placeholder="Enter some tags&hellip;" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="post-source">Source:</label>
<div class="form-input">
<input maxlength="200" type="text" name="source" id="post-source" placeholder="Where did you get this? (optional)" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="post-source">Source:</label>
<div class="form-input">
<input maxlength="200" type="text" name="source" id="post-source" placeholder="Where did you get this? (optional)" value=""/>
</div>
</div>
<% if (canUploadPostsAnonymously) { %>
<div class="form-row">
<label class="form-label" for="post-anonymous">Anonymity:</label>
<div class="form-input">
<input type="checkbox" id="post-anonymous" name="anonymous"/>
<label for="post-anonymous">
Don't show my name in this post
</label>
</div>
</div>
<% } %>
<% if (canUploadPostsAnonymously) { %>
<div class="form-row">
<label class="form-label" for="post-anonymous">Anonymity:</label>
<div class="form-input">
<input type="checkbox" id="post-anonymous" name="anonymous"/>
<label for="post-anonymous">
Don't show my name in this post
</label>
</div>
</div>
<% } %>
</form>
</div>
</div>
</div>
<div id="lightbox">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Preview">
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,282 +1,307 @@
<% var permaLink = (window.location.origin + '/' + window.location.pathname + '/data/posts/' + post.name).replace(/([^:])\/+/g, '$1/') %>
<%
var permaLink = '';
permaLink += window.location.origin + '/';
permaLink += window.location.pathname + '/';
permaLink += 'data/posts/' + post.name;
permaLink = permaLink.replace(/([^:])\/+/g, '$1/');
if (forceHttpInPermalinks > 0) {
permaLink = permaLink.replace('https', 'http');
}
%>
<div id="post-current-search-wrapper">
<div id="post-current-search">
<div class="left">
<a class="enabled">
<i class="fa fa-chevron-left"></i>
Next
</a>
</div>
<div id="post-current-search">
<div class="left">
<a class="enabled">
<i class="fa fa-chevron-left"></i>
Next
</a>
</div>
<div class="search">
<a href="#/posts/query=<%= query.query %>;order=<%= query.order %>">
Current search: <%= query.query || '-' %>
</a>
</div>
<div class="search">
<a class="enabled" href="<%= util.appendComplexRouteParam('#/posts', util.simplifySearchQuery({query: query.query, order: query.order})) %>">
Current search: <%= query.query || '-' %>
</a>
</div>
<div class="right">
<a class="enabled">
Previous
<i class="fa fa-chevron-right"></i>
</a>
</div>
</div>
<div class="right">
<a class="enabled">
Previous
<i class="fa fa-chevron-right"></i>
</a>
</div>
</div>
</div>
<div id="post-view-wrapper">
<div id="sidebar">
<ul class="essential">
<li>
<a class="download" href="<%= permaLink %>">
<i class="fa fa-download"></i>
<br/>
<%= post.contentExtension + ', ' + formatFileSize(post.originalFileSize) %>
</a>
</li>
<div id="sidebar">
<ul class="essential">
<% if (post.contentType !== 'youtube') { %>
<li>
<a class="download" href="<%= permaLink %>">
<i class="fa fa-download"></i>
<br/>
<%= post.contentExtension + ', ' + util.formatFileSize(post.originalFileSize) %>
</a>
</li>
<% } %>
<% if (isLoggedIn) { %>
<li>
<% if (hasFav) { %>
<a class="delete-favorite" href="#">
<i class="fa fa-heart"></i>
</a>
<% } else { %>
<a class="add-favorite" href="#">
<i class="fa fa-heart-o"></i>
</a>
<% } %>
</li>
<% if (isLoggedIn) { %>
<li>
<% if (hasFav) { %>
<a class="delete-favorite" href="#">
<i class="fa fa-heart"></i>
</a>
<% } else { %>
<a class="add-favorite" href="#">
<i class="fa fa-heart-o"></i>
</a>
<% } %>
</li>
<li>
<a class="score-up <% print(ownScore === 1 ? 'active' : '') %>" href="#">
<% if (ownScore === 1) { %>
<i class="fa fa-thumbs-up"></i>
<% } else { %>
<i class="fa fa-thumbs-o-up"></i>
<% } %>
</a>
</li>
<li>
<a class="score-up <% print(ownScore === 1 ? 'active' : '') %>" href="#">
<% if (ownScore === 1) { %>
<i class="fa fa-thumbs-up"></i>
<% } else { %>
<i class="fa fa-thumbs-o-up"></i>
<% } %>
</a>
</li>
<li>
<a class="score-down <% print(ownScore === -1 ? 'active' : '') %>" href="#">
<% if (ownScore === -1) { %>
<i class="fa fa-thumbs-down"></i>
<% } else { %>
<i class="fa fa-thumbs-o-down"></i>
<% } %>
</a>
</li>
<% } %>
</ul>
<li>
<a class="score-down <% print(ownScore === -1 ? 'active' : '') %>" href="#">
<% if (ownScore === -1) { %>
<i class="fa fa-thumbs-down"></i>
<% } else { %>
<i class="fa fa-thumbs-o-down"></i>
<% } %>
</a>
</li>
<% } %>
</ul>
<h1>Tags (<%= _.size(post.tags) %>)</h1>
<ul class="tags">
<% _.each(post.tags, function(tag) { %>
<li class="tag-category-<%= tag.category %>"><!--
--><a class="tag-edit" href="#/tag/<%= tag.name %>"><!--
--><i class="fa fa-tag"></i><!--
--></a><!--
<div class="box">
<h1>Tags (<%= _.size(post.tags) %>)</h1>
<ul class="tags">
<% _.each(post.tags, function(tag) { %>
<li class="tag-category-<%= tag.category %>"><!--
--><a class="tag-edit" href="#/tag/<%= tag.name %>"><!--
--><i class="fa fa-tag"></i><!--
--></a><!--
--><a class="post-search" href="#/posts/query=<%= tag.name %>"><!--
--><span class="tag-name"><%= tag.name %></span><!--
--><span class="usages"><%= (tag.usages) %></span>
</a>
</li>
<% }) %>
</ul>
--><a class="post-search" href="#/posts/query=<%= tag.name %>"><!--
--><span class="tag-name"><%= tag.name %></span><!--
--><span class="usages"><%= (tag.usages) %></span>
</a>
</li>
<% }) %>
</ul>
</div>
<h1>Details</h1>
<div class="box">
<h1>Details</h1>
<div class="author-box">
<% if (post.user.name) { %>
<a href="#/user/<%= post.user.name %>">
<% } %>
<div class="author-box">
<% if (post.user.name) { %>
<a href="#/user/<%= post.user.name %>">
<% } %>
<img width="40" height="40" class="author-avatar"
src="/data/thumbnails/40x40/avatars/<%= post.user.name || '!' %>"
alt="<%= post.user.name || 'Anonymous user' %>"/>
<img width="40" height="40" class="author-avatar"
src="/data/thumbnails/40x40/avatars/<%= post.user.name || '!' %>"
alt="<%= post.user.name || 'Anonymous user' %>"/>
<span class="author-name">
<%= post.user.name || 'Anonymous user' %>
</span>
<span class="author-name">
<%= post.user.name || 'Anonymous user' %>
</span>
<% if (post.user.name) { %>
</a>
<% } %>
<% if (post.user.name) { %>
</a>
<% } %>
<br/>
<br/>
<span class="date" title="<%= util.formatAbsoluteTime(post.creationTime) %>">
<%= util.formatRelativeTime(post.creationTime) %>
</span>
</div>
<span class="date"><%= formatRelativeTime(post.uploadTime) %></span>
</div>
<ul class="other-info">
<li>
Rating:
<span class="safety-<%= post.safety %>">
<%= post.safety %>
</span>
</li>
<ul class="other-info">
<% if (post.originalFileSize) { %>
<li>
File size:
<%= util.formatFileSize(post.originalFileSize) %>
</li>
<% } %>
<li>
Rating:
<span class="safety-<%= post.safety %>">
<%= post.safety %>
</span>
</li>
<% if (post.contentType == 'image') { %>
<li>
Image size:
<%= post.imageWidth + 'x' + post.imageHeight %>
</li>
<% } %>
<% if (post.originalFileSize) { %>
<li>
File size:
<%= formatFileSize(post.originalFileSize) %>
</li>
<% } %>
<% if (post.lastEditTime !== post.creationTime) { %>
<li>
Edited:
<span title="<%= util.formatAbsoluteTime(post.lastEditTime) %>">
<%= util.formatRelativeTime(post.lastEditTime) %>
</span>
</li>
<% } %>
<% if (post.contentType == 'image') { %>
<li>
Image size:
<%= post.imageWidth + 'x' + post.imageHeight %>
</li>
<% } %>
<% if (post.featureCount > 0) { %>
<li>
Featured: <%= post.featureCount %> <%= post.featureCount < 2 ? 'time' : 'times' %>
<small>(<%= util.formatRelativeTime(post.lastFeatureTime) %>)</small>
</li>
<% } %>
<% if (post.lastEditTime !== post.uploadTime) { %>
<li>
Edited:
<%= formatRelativeTime(post.lastEditTime) %>
</li>
<% } %>
<% if (post.source) { %>
<li><!--
--><% var link = post.source.match(/^(\/\/|https?:\/\/)/); %><!--
-->Source:&nbsp;<!--
--><% if (link) { %><!--
--><a href="<%= post.source %>"><!--
--><% } %><!--
--><%= post.source.trim() %><!--
--><% if (link) { %><!--
--></a><!--
--><% } %><!--
--></li>
<% } %>
<% if (post.featureCount > 0) { %>
<li>
Featured: <%= post.featureCount %> <%= post.featureCount < 2 ? 'time' : 'times' %>
<small>(<%= formatRelativeTime(post.lastFeatureTime) %>)</small>
</li>
<% } %>
<li>
Score: <%= post.score %>
</li>
</ul>
<% if (post.source) { %>
<li><!--
--><% var link = post.source.match(/^(\/\/|https?:\/\/)/); %><!--
-->Source:&nbsp;<!--
--><% if (link) { %><!--
--><a href="<%= post.source %>"><!--
--><% } %><!--
--><%= post.source.trim() %><!--
--><% if (link) { %><!--
--></a><!--
--><% } %><!--
--></li>
<% } %>
<% if (_.any(postFavorites)) { %>
<p>Favorites:</p>
<li>
Score: <%= post.score %>
</li>
</ul>
<ul class="favorites">
<% _.each(postFavorites, function(user) { %>
<li>
<a href="#/user/<%= user.name %>">
<img class="fav-avatar"
src="/data/thumbnails/25x25/avatars/<%= user.name || '!' %>"
alt="<%= user.name || 'Anonymous user' %>"/>
</a>
</li>
<% }) %>
</ul>
<% } %>
</div>
<% if (_.any(postFavorites)) { %>
<p>Favorites:</p>
<% if (_.any(post.relations)) { %>
<div class="box">
<h1>Related posts</h1>
<ul class="related">
<% _.each(post.relations, function(relatedPost) { %>
<li>
<a href="#/post/<%= relatedPost.id %>">
<%= relatedPost.idMarkdown %>
</a>
</li>
<% }) %>
</ul>
</div>
<% } %>
<ul class="favorites">
<% _.each(postFavorites, function(user) { %>
<li>
<a href="#/user/<%= user.name %>">
<img class="fav-avatar"
src="/data/thumbnails/25x25/avatars/<%= user.name || '!' %>"
alt="<%= user.name || 'Anonymous user' %>"/>
</a>
</li>
<% }) %>
</ul>
<% } %>
<div class="box">
<h1>Options</h1>
<ul class="operations">
<% if (_.any(editPrivileges)) { %>
<li>
<a class="edit" href="#">
Edit
</a>
</li>
<% } %>
<% if (_.any(post.relations)) { %>
<h1>Related posts</h1>
<ul class="related">
<% _.each(post.relations, function(relatedPost) { %>
<li>
<a href="#/post/<%= relatedPost.id %>">
<%= relatedPost.idMarkdown %>
</a>
</li>
<% }) %>
</ul>
<% } %>
<% if (privileges.canAddPostNotes && (post.contentType === 'image' || post.contentType === 'animation')) { %>
<li>
<a class="add-note" href="#">
Add new note
</a>
</li>
<% } %>
<% if (_.any(privileges) || _.any(editPrivileges) || post.contentType === 'image') { %>
<h1>Options</h1>
<% if (privileges.canDeletePosts) { %>
<li>
<a class="delete" href="#">
Delete
</a>
</li>
<% } %>
<ul class="operations">
<% if (_.any(editPrivileges)) { %>
<li>
<a class="edit" href="#">
Edit
</a>
</li>
<% } %>
<% if (privileges.canFeaturePosts) { %>
<li>
<a class="feature" href="#">
Feature
</a>
</li>
<% } %>
<% if (privileges.canAddPostNotes) { %>
<li>
<a class="add-note" href="#">
Add new note
</a>
</li>
<% } %>
<% if (privileges.canViewHistory) { %>
<li>
<a class="history" href="#">
History
</a>
</li>
<% } %>
<% if (privileges.canDeletePosts) { %>
<li>
<a class="delete" href="#">
Delete
</a>
</li>
<% } %>
<% if (post.contentType === 'image' || post.contentType === 'animation') { %>
<li>
<a href="http://iqdb.org/?url=<%= permaLink %>">
Search on IQDB
</a>
</li>
<% if (privileges.canFeaturePosts) { %>
<li>
<a class="feature" href="#">
Feature
</a>
</li>
<% } %>
<li>
<a href="https://www.google.com/searchbyimage?&image_url=<%= permaLink %>">
Search on Google Images
</a>
</li>
<% } %>
<% if (privileges.canViewHistory) { %>
<li>
<a class="history" href="#">
History
</a>
</li>
<% } %>
<li class="fit-mode">
Fit:
<a data-fit-mode="fit-width" href="#">width</a>,
<a data-fit-mode="fit-height" href="#">height</a>,
<a data-fit-mode="fit-both" href="#">both</a>,
<a data-fit-mode="original" href="#">original</a>
</li>
</ul>
</div>
</div>
<% if (post.contentType === 'image') { %>
<li>
<a href="http://iqdb.org/?url=<%= permaLink %>">
Search on IQDB
</a>
</li>
<div id="post-view">
<div class="messages"></div>
<li>
<a href="https://www.google.com/searchbyimage?&image_url=<%= permaLink %>">
Search on Google Images
</a>
</li>
<% } %>
</ul>
<% } %>
<div id="post-edit-target">
</div>
</div>
<div id="post-content-target">
</div>
<div id="post-view">
<div class="messages"></div>
<% if (privileges.canViewHistory) { %>
<div class="post-history-wrapper">
<h1>History</h1>
<%= historyTemplate({
history: postHistory,
util: util,
}) %>
</div>
<% } %>
<div id="post-edit-target">
</div>
<div id="post-content-target">
</div>
<% if (privileges.canViewHistory) { %>
<div class="post-history-wrapper">
<h1>History</h1>
<%= historyTemplate({
history: postHistory,
formatRelativeTime: formatRelativeTime
}) %>
</div>
<% } %>
<div id="post-comments-target">
</div>
</div>
<div id="post-comments-target">
</div>
</div>
</div>

View File

@ -1,50 +1,50 @@
<div id="registration-form">
<p>
Registered users can view more content,<br/>
upload files and add posts to favorites.
</p>
<p>
Registered users can view more content,<br/>
upload files and add posts to favorites.
</p>
<div class="messages"></div>
<div class="messages"></div>
<form class="form-wrapper">
<div class="form-row">
<label class="form-label" for="registration-user">User name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="userName" id="registration-user" placeholder="e.g. darth_vader" value=""/>
</div>
</div>
<form class="form-wrapper">
<div class="form-row">
<label class="form-label" for="registration-user">User name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="userName" id="registration-user" placeholder="e.g. darth_vader" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="registration-password">Password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password" id="registration-password" placeholder="e.g. &#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="registration-password">Password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password" id="registration-password" placeholder="e.g. &#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="registration-password-confirmation">Password (repeat):</label>
<div class="form-input">
<input autocomplete="off" type="password" name="passwordConfirmation" id="registration-password-confirmation" placeholder="e.g. &#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="registration-password-confirmation">Password (repeat):</label>
<div class="form-input">
<input autocomplete="off" type="password" name="passwordConfirmation" id="registration-password-confirmation" placeholder="e.g. &#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="registration-email">E-mail address:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="email" id="registration-email" placeholder="e.g. vader@empire.gov" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label" for="registration-email">E-mail address:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="email" id="registration-email" placeholder="e.g. vader@empire.gov" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Register</button>
</div>
</div>
</form>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Register</button>
</div>
</div>
</form>
<p id="email-info">
Your e-mail will be used to show your <a href="http://gravatar.com/">Gravatar</a>.<br/>
Leave blank for random Gravatar.
</p>
<p id="email-info">
Your e-mail will be used to show your <a href="http://gravatar.com/">Gravatar</a>.<br/>
Leave blank for random Gravatar.
</p>
</div>

View File

@ -1,21 +1,21 @@
<tr class="tag">
<td class="name tag-category-<%= tag.category %>">
<a href="#/tag/<%= tag.name %>"><%= tag.name %></a>
</td>
<td class="implications">
<%= _.pluck(tag.implications, 'name').join(' ') || '-' %>
</td>
<td class="suggestions">
<%= _.pluck(tag.suggestions, 'name').join(' ') || '-' %>
</td>
<td class="usages">
<%= tag.usages %>
</td>
<td class="banned">
<% if (tag.banned) { %>
<i class="fa fa-times"></i>
<% } else { %>
<i class="fa fa-check"></i>
<% } %>
</td>
<td class="name tag-category-<%= tag.category %>">
<a href="#/tag/<%= tag.name %>"><%= tag.name %></a>
</td>
<td class="implications">
<%= _.pluck(tag.implications, 'name').join(' ') || '-' %>
</td>
<td class="suggestions">
<%= _.pluck(tag.suggestions, 'name').join(' ') || '-' %>
</td>
<td class="usages">
<%= tag.usages %>
</td>
<td class="banned">
<% if (tag.banned) { %>
<i class="fa fa-times"></i>
<% } else { %>
<i class="fa fa-check"></i>
<% } %>
</td>
</tr>

Some files were not shown because too many files have changed in this diff Show More