servel 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46aac7ea1121e835e50af2f906be76d30fb73190171842ce642381585b334ea6
4
- data.tar.gz: 0f31b01220a763c8fcdd96561d76ee75bed06c2d052d7f5f521b8e9968f14791
3
+ metadata.gz: 2763001e5065c946d727b67fdf97e42a888d6794159528c72b2f15c7492c3409
4
+ data.tar.gz: d44b73a268aa7e69ea9b431ade3700a311b37906614fd8601effafc09473b2f4
5
5
  SHA512:
6
- metadata.gz: febe17fedca3812eb8e2a21cff9a0e869a8a3a683bb593e85dcde279b065bdc3b47ff0c0e9789367db331b2a9b3b1183c11ccbf533769914aad9154f730315a5
7
- data.tar.gz: 0313e123f72be5c37852fa9f1205a97ec30c44137525b8a2e776cc3527ad7bbc9e080f565632814c099d800f874f9dcfc33f1b4287881cb98cacfd936d8fdb6e
6
+ metadata.gz: ea0615e5c1661f40afd3ebf639e6d751db50989506fec1434c81f72ca3aaa3107c0eb5a8aa5f40e899e7d94ca62c99332e0998389c4a23296c910f8dd2c43750
7
+ data.tar.gz: 0071e811b8c7c5a401b5a060c297a686616e311a9bd6726a4e4bd5d6ed6626ebf25cd193ea2202dbeaff9a9ca851bfc4f19de73ad819a5245e33c196cb0c3ede
data/lib/servel.rb CHANGED
@@ -7,6 +7,7 @@ require 'lru_redux'
7
7
 
8
8
  require 'thread'
9
9
  require 'pathname'
10
+ require 'json'
10
11
 
11
12
  module Servel
12
13
  def self.build_app(path_map)
@@ -22,6 +23,7 @@ require "servel/instrumentation"
22
23
  require "servel/entry"
23
24
  require "servel/entry_factory"
24
25
  require "servel/haml_context"
26
+ require "servel/entries"
25
27
  require "servel/index"
26
28
  require "servel/app"
27
29
  require "servel/home_app"
data/lib/servel/app.rb CHANGED
@@ -26,7 +26,12 @@ class Servel::App
26
26
  return [404, {}, []] unless fs_path.exist?
27
27
 
28
28
  request = Rack::Request.new(env)
29
- Servel::Index.new(url_root: url_root, url_path: url_path, fs_path: fs_path, params: request.params).render
29
+
30
+ if json_request?(request)
31
+ Servel::Entries.new(url_root: url_root, url_path: url_path, fs_path: fs_path, params: request.params).render
32
+ else
33
+ Servel::Index.new(url_root: url_root, url_path: url_path).render
34
+ end
30
35
  end
31
36
 
32
37
  def redirect(location)
@@ -39,6 +44,10 @@ class Servel::App
39
44
  Rack::Utils.clean_path_info(url_path)
40
45
  end
41
46
 
47
+ def json_request?(request)
48
+ Rack::Utils.best_q_match(request.get_header("HTTP_ACCEPT"), ['text/html', 'application/json']) == 'application/json'
49
+ end
50
+
42
51
  def try_encode(string)
43
52
  return string if string.encoding == UTF_8
44
53
  string.encode(UTF_8)
@@ -0,0 +1,68 @@
1
+ class Servel::Entries
2
+ extend Servel::Instrumentation
3
+ RENDER_CACHE = LruRedux::ThreadSafeCache.new(100)
4
+ SORT_METHODS = ["name", "mtime", "size", "type"]
5
+ SORT_DIRECTIONS = ["asc", "desc"]
6
+
7
+ def initialize(url_root:, url_path:, fs_path:, params:)
8
+ @url_root = url_root
9
+ @url_path = url_path
10
+ @fs_path = fs_path
11
+ @params = params
12
+ end
13
+
14
+ def render
15
+ RENDER_CACHE.getset(render_cache_key) { [200, {}, [entries.to_json]] }
16
+ end
17
+
18
+ def render_cache_key
19
+ @render_cache_key ||= [@fs_path.to_s, @fs_path.mtime.to_i, sort_method, sort_direction].join("-")
20
+ end
21
+
22
+ def entries
23
+ children = @fs_path.children.map { |path| Servel::EntryFactory.for(path) }.compact
24
+ special_entries + apply_sort(children.select(&:directory?)) + apply_sort(children.select(&:file?))
25
+ end
26
+
27
+ def sort_method
28
+ param = @params["_servel_sort_method"]
29
+ param = "name" unless SORT_METHODS.include?(param)
30
+ param
31
+ end
32
+
33
+ def sort_direction
34
+ param = @params["_servel_sort_direction"]
35
+ param = "asc" unless SORT_DIRECTIONS.include?(param)
36
+ param
37
+ end
38
+
39
+ def special_entries
40
+ list = []
41
+ list << Servel::EntryFactory.home("/") if @url_root != ""
42
+
43
+ unless @url_path == "/"
44
+ list << Servel::EntryFactory.top(@url_root == "" ? "/" : @url_root)
45
+ list << Servel::EntryFactory.parent("../")
46
+ end
47
+
48
+ list
49
+ end
50
+
51
+ def apply_sort(entries)
52
+ entries = case sort_method
53
+ when "name"
54
+ Naturalsorter::Sorter.sort_by_method(entries, :name, true)
55
+ when "mtime"
56
+ entries.sort_by { |entry| entry.mtime }
57
+ when "size"
58
+ entries.sort_by { |entry| entry.size || 0 }
59
+ when "type"
60
+ entries.sort_by { |entry| entry.type }
61
+ end
62
+
63
+ entries.reverse! if sort_direction == "desc"
64
+ entries
65
+ end
66
+
67
+ instrument :render, :entries, :apply_sort
68
+ end
data/lib/servel/entry.rb CHANGED
@@ -1,27 +1,45 @@
1
- class Servel::Entry
2
- attr_reader :ftype, :type, :media_type, :listing_classes, :icon, :href, :name, :size, :mtime
3
-
4
- def initialize(ftype:, type:, media_type: nil, listing_classes:, icon:, href:, name:, size: nil, mtime: nil)
5
- @ftype = ftype
6
- @type = type
7
- @media_type = media_type
8
- @listing_classes = listing_classes
9
- @icon = icon
10
- @href = href
11
- @name = name
12
- @size = size
13
- @mtime = mtime
14
- end
15
-
16
- def directory?
17
- @ftype == :directory
18
- end
19
-
20
- def file?
21
- @ftype == :file
22
- end
23
-
24
- def media?
25
- !@media_type.nil?
26
- end
1
+ class Servel::Entry
2
+ extend Servel::Instrumentation
3
+
4
+ attr_reader :ftype, :type, :media_type, :listing_classes, :icon, :href, :name, :size, :mtime
5
+
6
+ def initialize(ftype:, type:, media_type: nil, listing_classes:, icon:, href:, name:, size: nil, mtime: nil)
7
+ @ftype = ftype
8
+ @type = type
9
+ @media_type = media_type
10
+ @listing_classes = listing_classes
11
+ @icon = icon
12
+ @href = href
13
+ @name = name
14
+ @size = size
15
+ @mtime = mtime
16
+ end
17
+
18
+ def directory?
19
+ @ftype == :directory
20
+ end
21
+
22
+ def file?
23
+ @ftype == :file
24
+ end
25
+
26
+ def media?
27
+ !@media_type.nil?
28
+ end
29
+
30
+ def as_json(*)
31
+ {
32
+ icon: @icon,
33
+ href: Rack::Utils.escape_path(@href),
34
+ class: @listing_classes,
35
+ media_type: @media_type,
36
+ name: @name,
37
+ type: @type,
38
+ size: @size.nil? ? "-" : @size,
39
+ mtime: @mtime.nil? ? "-" : @mtime.strftime("%e %b %Y %l:%M %p"),
40
+ media: media?
41
+ }
42
+ end
43
+
44
+ instrument :as_json
27
45
  end
@@ -1,54 +1,38 @@
1
- class Servel::HamlContext
2
- extend Servel::Instrumentation
3
- include ActiveSupport::NumberHelper
4
-
5
- LOCK = Mutex.new
6
-
7
- def self.render(template, locals)
8
- [200, {}, [new.render(template, locals)]]
9
- end
10
-
11
- def initialize
12
- @build_path = Pathname.new(__FILE__).dirname.realpath + 'templates'
13
- end
14
-
15
- def render(template, locals = {})
16
- haml_engine(template).render(self, locals)
17
- end
18
-
19
- def partial(name, locals = {})
20
- render("_#{name}.haml", locals)
21
- end
22
-
23
- def include(path)
24
- (@build_path + path).read
25
- end
26
-
27
- def sort_attrs(sort, current_method)
28
- data = { sort_method: current_method, sort_direction: "asc" }
29
- classes = ["sortable"]
30
- if sort[:method] == current_method
31
- data[:sort_active] = true
32
- data[:sort_direction] = sort[:direction]
33
- classes << "sort-active"
34
- classes << "sort-#{sort[:direction]}"
35
- end
36
-
37
- {
38
- data: data,
39
- class: classes
40
- }
41
- end
42
-
43
- def haml_engine(path)
44
- LOCK.synchronize do
45
- @@haml_engine_cache ||= {}
46
- unless @@haml_engine_cache.key?(path)
47
- @@haml_engine_cache[path] = Hamlit::Template.new(filename: path) { include(path) }
48
- end
49
- @@haml_engine_cache[path]
50
- end
51
- end
52
-
53
- instrument :render, :partial, :include
1
+ class Servel::HamlContext
2
+ extend Servel::Instrumentation
3
+ include ActiveSupport::NumberHelper
4
+
5
+ LOCK = Mutex.new
6
+
7
+ def self.render(template, locals)
8
+ [200, {}, [new.render(template, locals)]]
9
+ end
10
+
11
+ def initialize
12
+ @build_path = Pathname.new(__FILE__).dirname.realpath + 'templates'
13
+ end
14
+
15
+ def render(template, locals = {})
16
+ haml_engine(template).render(self, locals)
17
+ end
18
+
19
+ def partial(name, locals = {})
20
+ render("_#{name}.haml", locals)
21
+ end
22
+
23
+ def include(path)
24
+ (@build_path + path).read
25
+ end
26
+
27
+ def haml_engine(path)
28
+ LOCK.synchronize do
29
+ @@haml_engine_cache ||= {}
30
+ unless @@haml_engine_cache.key?(path)
31
+ @@haml_engine_cache[path] = Hamlit::Template.new(filename: path) { include(path) }
32
+ end
33
+ @@haml_engine_cache[path]
34
+ end
35
+ end
36
+
37
+ instrument :render, :partial, :include
54
38
  end
data/lib/servel/index.rb CHANGED
@@ -1,80 +1,21 @@
1
1
  class Servel::Index
2
2
  extend Servel::Instrumentation
3
- RENDER_CACHE = LruRedux::ThreadSafeCache.new(100)
4
- SORT_METHODS = ["name", "mtime", "size", "type"]
5
- SORT_DIRECTIONS = ["asc", "desc"]
6
3
 
7
- def initialize(url_root:, url_path:, fs_path:, params:)
4
+ def initialize(url_root:, url_path:)
8
5
  @url_root = url_root
9
6
  @url_path = url_path
10
- @fs_path = fs_path
11
- @params = params
12
7
  end
13
8
 
14
9
  def render
15
- RENDER_CACHE.getset(render_cache_key) { Servel::HamlContext.render('index.haml', locals) }
16
- end
17
-
18
- def render_cache_key
19
- @render_cache_key ||= [@fs_path.to_s, @fs_path.mtime.to_i, sort_method, sort_direction].join("-")
10
+ Servel::HamlContext.render('index.haml', locals)
20
11
  end
21
12
 
22
13
  def locals
23
14
  {
24
15
  url_root: @url_root,
25
- url_path: @url_path,
26
- entries: entries,
27
- sort: {
28
- method: sort_method,
29
- direction: sort_direction
30
- }
16
+ url_path: @url_path
31
17
  }
32
18
  end
33
19
 
34
- def entries
35
- children = @fs_path.children.map { |path| Servel::EntryFactory.for(path) }.compact
36
- special_entries + apply_sort(children.select(&:directory?)) + apply_sort(children.select(&:file?))
37
- end
38
-
39
- def sort_method
40
- param = @params["_servel_sort_method"]
41
- param = "name" unless SORT_METHODS.include?(param)
42
- param
43
- end
44
-
45
- def sort_direction
46
- param = @params["_servel_sort_direction"]
47
- param = "asc" unless SORT_DIRECTIONS.include?(param)
48
- param
49
- end
50
-
51
- def special_entries
52
- list = []
53
- list << Servel::EntryFactory.home("/") if @url_root != ""
54
-
55
- unless @url_path == "/"
56
- list << Servel::EntryFactory.top(@url_root == "" ? "/" : @url_root)
57
- list << Servel::EntryFactory.parent("../")
58
- end
59
-
60
- list
61
- end
62
-
63
- def apply_sort(entries)
64
- entries = case sort_method
65
- when "name"
66
- Naturalsorter::Sorter.sort_by_method(entries, :name, true)
67
- when "mtime"
68
- entries.sort_by { |entry| entry.mtime }
69
- when "size"
70
- entries.sort_by { |entry| entry.size || 0 }
71
- when "type"
72
- entries.sort_by { |entry| entry.type }
73
- end
74
-
75
- entries.reverse! if sort_direction == "desc"
76
- entries
77
- end
78
-
79
- instrument :render, :locals, :entries, :apply_sort
20
+ instrument :render, :locals
80
21
  end
@@ -1,25 +1,17 @@
1
1
  %h1 Listing of #{url_root}#{url_path}
2
+ #search-wrapper
3
+ %input#search{type: 'search'}
2
4
  .table-wrapper
3
5
  %table
4
6
  %thead
5
7
  %tr
6
- %th{sort_attrs(sort, "name")}
8
+ %th.sortable.sort-active.sort-asc{data: { sort_method: 'name', sort_direction: 'asc' }}
7
9
  %a{href: "#"} Name
8
10
  %th.new-tab
9
- %th.type{sort_attrs(sort, "type")}
11
+ %th.type.sortable{data: { sort_method: 'type', sort_direction: 'asc'}}
10
12
  %a{href: "#"} Type
11
- %th.size{sort_attrs(sort, "size")}
13
+ %th.size.sortable{data: { sort_method: 'size', sort_direction: 'asc'}}
12
14
  %a{href: "#"} Size
13
- %th.modified{sort_attrs(sort, "mtime")}
15
+ %th.modified.sortable{data: { sort_method: 'mtime', sort_direction: 'asc'}}
14
16
  %a{href: "#"} Modified
15
- %tbody
16
- - entries.each do |file|
17
- %tr
18
- %td
19
- %span.icon= file.icon
20
- %a.default{href: Rack::Utils.escape_path(file.href), class: file.listing_classes, data: { type: file.media_type }}= file.name
21
- %td
22
- %a.new-tab{href: Rack::Utils.escape_path(file.href), class: file.listing_classes, target: "_blank"} (New tab)
23
- %td= file.type
24
- %td= file.size.nil? ? "-" : number_to_human_size(file.size)
25
- %td= file.mtime.nil? ? "-" : file.mtime.strftime("%e %b %Y %l:%M %p")
17
+ #listing-container
@@ -22,4 +22,19 @@ a {
22
22
 
23
23
  a:hover, a:focus, a:active {
24
24
  text-decoration: underline;
25
+ }
26
+
27
+ #loading {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ position: fixed;
32
+ top: 0;
33
+ right: 0;
34
+ bottom: 0;
35
+ left: 0;
36
+ z-index: 2;
37
+ background-color: rgba(255, 255, 255, 0.6);
38
+ font-size: 24px;
39
+ font-weight: bold;
25
40
  }
@@ -1,6 +1,6 @@
1
1
  #gallery {
2
2
  background: #333;
3
- display: flex;
3
+ display: none;
4
4
  align-items: center;
5
5
  justify-content: center;
6
6
  }
@@ -49,6 +49,7 @@
49
49
  top: 0;
50
50
  right: 0;
51
51
  left: 0;
52
+ pointer-events: none;
52
53
  }
53
54
 
54
55
  .paginator {
@@ -63,6 +64,7 @@
63
64
  display: flex;
64
65
  align-items: center;
65
66
  justify-content: center;
67
+ pointer-events: auto;
66
68
  }
67
69
 
68
70
  .paginator:active {
@@ -1,4 +1,5 @@
1
1
  #listing {
2
+ display: none;
2
3
  padding: 10px;
3
4
  position: relative;
4
5
  z-index: 1;
@@ -9,6 +10,21 @@
9
10
  margin-top: 0;
10
11
  }
11
12
 
13
+ #search-wrapper {
14
+ text-align: right;
15
+ margin-bottom: 20px;
16
+ }
17
+
18
+ #search {
19
+ width: 300px;
20
+ max-width: 100%;
21
+ padding: 5px;
22
+ font-family: 'Open Sans', Helvetica, Arial, sans-serif;
23
+ font-size: 16px;
24
+ line-height: 1.4;
25
+ border: 1px solid #ddd;
26
+ }
27
+
12
28
  .table-wrapper {
13
29
  overflow: auto;
14
30
  }
@@ -19,6 +35,11 @@ table {
19
35
  border-collapse: collapse;
20
36
  border: 1px solid #ddd;
21
37
  table-layout: fixed;
38
+ will-change: transform;
39
+ }
40
+
41
+ #listing-container table, #listing-container tr:first-child td {
42
+ border-top: none;
22
43
  }
23
44
 
24
45
  th {
@@ -30,23 +51,27 @@ th, td {
30
51
  border: 1px solid #ddd;
31
52
  }
32
53
 
54
+ tbody {
55
+ vertical-align: top;
56
+ }
57
+
33
58
  tbody > tr:nth-of-type(odd) {
34
59
  background-color: #f9f9f9;
35
60
  }
36
61
 
37
- th.new-tab {
62
+ th.new-tab, td.new-tab {
38
63
  width: 6em;
39
64
  }
40
65
 
41
- th.type {
42
- width: 4em;
66
+ th.type, td.type {
67
+ width: 5em;
43
68
  }
44
69
 
45
- th.size {
70
+ th.size, td.size {
46
71
  width: 6em;
47
72
  }
48
73
 
49
- th.modified {
74
+ th.modified, td.modified {
50
75
  width: 12em;
51
76
  }
52
77
 
@@ -12,11 +12,11 @@
12
12
  #{include('css/gallery-text.css')}
13
13
 
14
14
  :javascript
15
- #{include('js/url-search-params.js')}
16
15
  #{include('js/listing.js')}
17
16
  #{include('js/ume.js')}
18
17
  #{include('js/gallery.js')}
18
+ #{include('js/index.js')}
19
19
  %body
20
- - if entries.any? { |f| f.media? }
21
- #gallery!= partial('gallery')
22
- #listing!= partial('listing', { url_root: url_root, url_path: url_path, entries: entries, sort: sort })
20
+ #gallery!= partial('gallery')
21
+ #listing!= partial('listing', { url_root: url_root, url_path: url_path })
22
+ #loading Loading...
@@ -1,21 +1,11 @@
1
- var $;
1
+ "use strict";
2
2
 
3
3
  var Gallery = (function() {
4
- var urls = [];
5
- var types = [];
4
+ var $;
5
+ var $gallery;
6
+ var mediaEntries = [];
6
7
  var currentIndex = 0;
7
8
  var layoutItemMax = false;
8
- var $gallery;
9
-
10
- function initItems() {
11
- var links = document.querySelectorAll("#listing a.default.media");
12
- for(var i = 0; i < links.length; i++) {
13
- var link = links[i];
14
-
15
- urls.push(link.href);
16
- types.push(link.dataset.type);
17
- }
18
- }
19
9
 
20
10
  function renderText(url) {
21
11
  var http = new XMLHttpRequest();
@@ -39,8 +29,10 @@ var Gallery = (function() {
39
29
  function render() {
40
30
  clearContent();
41
31
 
42
- var url = urls[currentIndex];
43
- var type = types[currentIndex];
32
+ var entry = mediaEntries[currentIndex];
33
+
34
+ var url = entry.href;
35
+ var type = entry.media_type;
44
36
 
45
37
  $gallery.classList.add(type);
46
38
 
@@ -60,7 +52,7 @@ var Gallery = (function() {
60
52
 
61
53
  function clamp(index) {
62
54
  if(index == null || isNaN(index) || index < 0) return 0;
63
- if(index >= urls.length) return urls.length - 1;
55
+ if(index >= mediaEntries.length) return mediaEntries.length - 1;
64
56
  return index;
65
57
  }
66
58
 
@@ -89,7 +81,10 @@ var Gallery = (function() {
89
81
  }
90
82
 
91
83
  function jump(url) {
92
- go(urls.indexOf(url));
84
+ var index = mediaEntries.findIndex(function(entry) {
85
+ return entry.href == url;
86
+ });
87
+ go(index);
93
88
  }
94
89
 
95
90
  function atBottom() {
@@ -102,7 +97,7 @@ var Gallery = (function() {
102
97
 
103
98
  if(e.target.matches("a.media:not(.new-tab)")) {
104
99
  e.preventDefault();
105
- jump(e.target.href);
100
+ jump(e.target.dataset.url);
106
101
  }
107
102
  else if(e.target.matches("#page-back")) {
108
103
  e.stopPropagation();
@@ -132,6 +127,8 @@ var Gallery = (function() {
132
127
  });
133
128
 
134
129
  window.addEventListener("keydown", function(e) {
130
+ if(e.target == $("#search")) return;
131
+
135
132
  if(e.keyCode == 39 || ((e.keyCode == 32 || e.keyCode == 13) && atBottom())) {
136
133
  e.preventDefault();
137
134
  next();
@@ -159,19 +156,35 @@ var Gallery = (function() {
159
156
  layout();
160
157
  }
161
158
 
162
- function init() {
163
- $gallery = $("#gallery");
159
+ function onEntriesInit() {
160
+ onEntriesUpdate();
164
161
 
165
- initItems();
166
- initEvents();
167
- initLayout();
168
- render();
162
+ if(mediaEntries.length > 0) {
163
+ $gallery.style.display = "flex";
164
+
165
+ initEvents();
166
+ initLayout();
167
+ }
168
+ }
169
+
170
+ function onEntriesUpdate() {
171
+ currentIndex = 0;
172
+ mediaEntries = [];
173
+ for(var i = 0; i < window.entries.length; i++) {
174
+ if(window.entries[i].media) mediaEntries.push(window.entries[i]);
175
+ }
176
+
177
+ if(mediaEntries.length > 0) render();
169
178
  }
170
179
 
180
+ window.addEventListener("DOMContentLoaded", function() {
181
+ $ = document.querySelector.bind(document);
182
+ $gallery = $("#gallery");
183
+ });
184
+
171
185
  return {
172
- init: init,
173
- render: render,
174
- clamp: clamp,
186
+ onEntriesInit: onEntriesInit,
187
+ onEntriesUpdate: onEntriesUpdate,
175
188
  go: go,
176
189
  prev: prev,
177
190
  next: next,
@@ -180,11 +193,3 @@ var Gallery = (function() {
180
193
  jump: jump
181
194
  };
182
195
  })();
183
-
184
- function initGallery() {
185
- $ = document.querySelector.bind(document);
186
- if(!$("#gallery")) return;
187
- Gallery.init();
188
- }
189
-
190
- window.addEventListener("DOMContentLoaded", initGallery);
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ var Index = (function() {
4
+ var $;
5
+ var inited = false;
6
+
7
+ function entriesURL() {
8
+ var sortable = $("th.sortable.sort-active");
9
+ return `${location.pathname}?_servel_sort_method=${sortable.dataset.sortMethod}&_servel_sort_direction=${sortable.dataset.sortDirection}`;
10
+ }
11
+
12
+ function onEntriesLoad(callback) {
13
+ if(inited) {
14
+ Gallery.onEntriesUpdate();
15
+ Listing.onEntriesUpdate();
16
+ }
17
+ else {
18
+ inited = true;
19
+ Gallery.onEntriesInit();
20
+ Listing.onEntriesInit();
21
+ }
22
+
23
+ $("#loading").style.display = "none";
24
+ if(callback) callback();
25
+ }
26
+
27
+ function loadEntries(callback) {
28
+ $("#loading").style.display = "flex";
29
+
30
+ var http = new XMLHttpRequest();
31
+ http.open("GET", entriesURL());
32
+
33
+ http.onreadystatechange = function() {
34
+ if(http.readyState === 4 && http.status === 200) {
35
+ window.entries = JSON.parse(http.responseText);
36
+ setTimeout(function() {
37
+ onEntriesLoad(callback);
38
+ }, 0);
39
+ }
40
+ };
41
+
42
+ http.setRequestHeader("Accept", "application/json");
43
+ http.send();
44
+ }
45
+
46
+ window.addEventListener("DOMContentLoaded", function() {
47
+ $ = document.querySelector.bind(document);
48
+ loadEntries();
49
+ });
50
+
51
+ return {
52
+ loadEntries: loadEntries
53
+ };
54
+ })();
@@ -1,26 +1,161 @@
1
- function initListing() {
2
- $ = document.querySelector.bind(document);
3
-
4
- function applySort(sortable) {
5
- var params = new URLSearchParams(location.search.slice(1));
6
- params.set("_servel_sort_method", sortable.dataset.sortMethod);
7
- params.set("_servel_sort_direction", sortable.dataset.sortDirection);
8
-
9
- if('sortActive' in sortable.dataset) {
10
- params.set("_servel_sort_direction", sortable.dataset.sortDirection == "asc" ? "desc" : "asc");
11
- }
12
-
13
- location.href = location.pathname + "?" + params.toString();
14
- }
15
-
16
- document.body.addEventListener("click", function(e) {
17
- if(!e.target) return;
18
-
19
- if(e.target.closest("th.sortable")) {
20
- e.preventDefault();
21
- applySort(e.target.closest("th.sortable"));
22
- }
23
- });
24
- }
25
-
26
- window.addEventListener("DOMContentLoaded", initListing);
1
+ "use strict";
2
+
3
+ var Listing = (function() {
4
+ var $;
5
+ var $container;
6
+ var filteredEntries = [];
7
+ var perPage = 99;
8
+ var currentIndex = 0;
9
+ var moreContent = true;
10
+ var scrollDebounce = false;
11
+
12
+ function escapeHTML(unsafe) {
13
+ if(unsafe == null) return "";
14
+ return unsafe.toString()
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&#039;");
20
+ }
21
+
22
+ function HTMLSafe(pieces) {
23
+ var result = pieces[0];
24
+ var substitutions = [].slice.call(arguments, 1);
25
+ for(var i = 0; i < substitutions.length; ++i) result += escapeHTML(substitutions[i]) + pieces[i + 1];
26
+ return result;
27
+ }
28
+
29
+ function renderRow(file) {
30
+ return HTMLSafe`
31
+ <tr>
32
+ <td class="name">
33
+ <span class="icon">${file.icon}</span>
34
+ <a href="${file.href}" class="default ${file.class}" data-url="${file.href}" data-type="${file.media_type}">${file.name}</a>
35
+ </td>
36
+ <td class="new-tab">
37
+ <a href="${file.href}" class="new-tab ${file.class}" target="_blank">(New tab)</a>
38
+ </td>
39
+ <td class="type">${file.type}</td>
40
+ <td class="size">${file.size}</td>
41
+ <td class="modified">${file.mtime}</td>
42
+ </tr>
43
+ `;
44
+ }
45
+
46
+ function renderTable(currentEntries) {
47
+ var rows = currentEntries.map(function(entry) {
48
+ return renderRow(entry);
49
+ });
50
+
51
+ return `
52
+ <table>
53
+ <tbody>
54
+ ${rows.join("")}
55
+ </tbody>
56
+ </table>
57
+ `;
58
+ }
59
+
60
+ function render() {
61
+ var currentEntries = filteredEntries.slice(currentIndex, currentIndex + perPage);
62
+ $container.insertAdjacentHTML("beforeend", renderTable(currentEntries));
63
+ }
64
+
65
+ function atBottom() {
66
+ return (window.scrollY + window.innerHeight) == document.body.scrollHeight;
67
+ }
68
+
69
+ function onScrolled() {
70
+ if(atBottom() && moreContent) {
71
+ currentIndex += perPage;
72
+ if(currentIndex >= filteredEntries.length) moreContent = false;
73
+ render();
74
+ }
75
+ scrollDebounce = false;
76
+ }
77
+
78
+ function applySort(sortable) {
79
+ var previousSortable = $("th.sortable.sort-active");
80
+ previousSortable.classList.remove("sort-active", "sort-asc", "sort-desc");
81
+
82
+ if(sortable == previousSortable) {
83
+ sortable.dataset.sortDirection = sortable.dataset.sortDirection == "asc" ? "desc" : "asc";
84
+ }
85
+
86
+ sortable.classList.add("sort-active", "sort-" + sortable.dataset.sortDirection);
87
+
88
+ Index.loadEntries(function() {
89
+ sortable.scrollIntoView();
90
+ });
91
+ }
92
+
93
+ function updateFilteredEntries(needle) {
94
+ if(needle == "") {
95
+ filteredEntries = window.entries;
96
+ }
97
+ else {
98
+ filteredEntries = [];
99
+
100
+ for(var i = 0; i < window.entries.length; i++) {
101
+ var entry = window.entries[i];
102
+ if(entry.name.toLowerCase().includes(needle.toLowerCase())) filteredEntries.push(entry);
103
+ }
104
+ }
105
+ }
106
+
107
+ function applyFilter(needle) {
108
+ updateFilteredEntries(needle);
109
+
110
+ $container.innerHTML = "";
111
+ currentIndex = 0;
112
+ moreContent = true;
113
+ render();
114
+ }
115
+
116
+ function initEvents() {
117
+ window.addEventListener("scroll", function(e) {
118
+ if(!scrollDebounce) {
119
+ scrollDebounce = true;
120
+ setTimeout(onScrolled, 0);
121
+ }
122
+ });
123
+
124
+ document.body.addEventListener("click", function(e) {
125
+ if(!e.target) return;
126
+
127
+ if(e.target.closest("th.sortable")) {
128
+ e.preventDefault();
129
+ applySort(e.target.closest("th.sortable"));
130
+ }
131
+ });
132
+
133
+ $("#search").addEventListener("keyup", function(e) {
134
+ e.stopPropagation();
135
+
136
+ if(e.keyCode == 13) {
137
+ applyFilter($("#search").value);
138
+ }
139
+ });
140
+ }
141
+
142
+ function onEntriesInit() {
143
+ onEntriesUpdate();
144
+ $("#listing").style.display = "block";
145
+ initEvents();
146
+ }
147
+
148
+ function onEntriesUpdate() {
149
+ applyFilter($("#search").value);
150
+ }
151
+
152
+ window.addEventListener("DOMContentLoaded", function() {
153
+ $ = document.querySelector.bind(document);
154
+ $container = $("#listing-container");
155
+ });
156
+
157
+ return {
158
+ onEntriesInit: onEntriesInit,
159
+ onEntriesUpdate: onEntriesUpdate
160
+ };
161
+ })();
@@ -1,3 +1,3 @@
1
1
  module Servel
2
- VERSION = "0.19.0"
2
+ VERSION = "0.20.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brenton "B-Train" Fletcher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-22 00:00:00.000000000 Z
11
+ date: 2018-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -141,6 +141,7 @@ files:
141
141
  - lib/servel.rb
142
142
  - lib/servel/app.rb
143
143
  - lib/servel/cli.rb
144
+ - lib/servel/entries.rb
144
145
  - lib/servel/entry.rb
145
146
  - lib/servel/entry_factory.rb
146
147
  - lib/servel/haml_context.rb
@@ -158,9 +159,9 @@ files:
158
159
  - lib/servel/templates/home.haml
159
160
  - lib/servel/templates/index.haml
160
161
  - lib/servel/templates/js/gallery.js
162
+ - lib/servel/templates/js/index.js
161
163
  - lib/servel/templates/js/listing.js
162
164
  - lib/servel/templates/js/ume.js
163
- - lib/servel/templates/js/url-search-params.js
164
165
  - lib/servel/version.rb
165
166
  - servel.gemspec
166
167
  homepage: http://bloople.net/
@@ -1,2 +0,0 @@
1
- /*! (C) Andrea Giammarchi - Mit Style License */
2
- var URLSearchParams=URLSearchParams||function(){"use strict";function URLSearchParams(query){var index,key,value,pairs,i,length,dict=Object.create(null);this[secret]=dict;if(!query)return;if(typeof query==="string"){if(query.charAt(0)==="?"){query=query.slice(1)}for(pairs=query.split("&"),i=0,length=pairs.length;i<length;i++){value=pairs[i];index=value.indexOf("=");if(-1<index){appendTo(dict,decode(value.slice(0,index)),decode(value.slice(index+1)))}else if(value.length){appendTo(dict,decode(value),"")}}}else{if(isArray(query)){for(i=0,length=query.length;i<length;i++){value=query[i];appendTo(dict,value[0],value[1])}}else{for(key in query){appendTo(dict,key,query[key])}}}}var isArray=Array.isArray,URLSearchParamsProto=URLSearchParams.prototype,find=/[!'\(\)~]|%20|%00/g,plus=/\+/g,replace={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"},replacer=function(match){return replace[match]},secret="__URLSearchParams__:"+Math.random();function appendTo(dict,name,value){if(name in dict){dict[name].push(""+value)}else{dict[name]=isArray(value)?value:[""+value]}}function decode(str){return decodeURIComponent(str.replace(plus," "))}function encode(str){return encodeURIComponent(str).replace(find,replacer)}URLSearchParamsProto.append=function append(name,value){appendTo(this[secret],name,value)};URLSearchParamsProto["delete"]=function del(name){delete this[secret][name]};URLSearchParamsProto.get=function get(name){var dict=this[secret];return name in dict?dict[name][0]:null};URLSearchParamsProto.getAll=function getAll(name){var dict=this[secret];return name in dict?dict[name].slice(0):[]};URLSearchParamsProto.has=function has(name){return name in this[secret]};URLSearchParamsProto.set=function set(name,value){this[secret][name]=[""+value]};URLSearchParamsProto.forEach=function forEach(callback,thisArg){var dict=this[secret];Object.getOwnPropertyNames(dict).forEach(function(name){dict[name].forEach(function(value){callback.call(thisArg,value,name,this)},this)},this)};URLSearchParamsProto.toJSON=function toJSON(){return{}};URLSearchParamsProto.toString=function toString(){var dict=this[secret],query=[],i,key,name,value;for(key in dict){name=encode(key);for(i=0,value=dict[key];i<value.length;i++){query.push(name+"="+encode(value[i]))}}return query.join("&")};var dP=Object.defineProperty,gOPD=Object.getOwnPropertyDescriptor,createSearchParamsPollute=function(search){function append(name,value){URLSearchParamsProto.append.call(this,name,value);name=this.toString();search.set.call(this._usp,name?"?"+name:"")}function del(name){URLSearchParamsProto["delete"].call(this,name);name=this.toString();search.set.call(this._usp,name?"?"+name:"")}function set(name,value){URLSearchParamsProto.set.call(this,name,value);name=this.toString();search.set.call(this._usp,name?"?"+name:"")}return function(sp,value){sp.append=append;sp["delete"]=del;sp.set=set;return dP(sp,"_usp",{configurable:true,writable:true,value:value})}},createSearchParamsCreate=function(polluteSearchParams){return function(obj,sp){dP(obj,"_searchParams",{configurable:true,writable:true,value:polluteSearchParams(sp,obj)});return sp}},updateSearchParams=function(sp){var append=sp.append;sp.append=URLSearchParamsProto.append;URLSearchParams.call(sp,sp._usp.search.slice(1));sp.append=append},verifySearchParams=function(obj,Class){if(!(obj instanceof Class))throw new TypeError("'searchParams' accessed on an object that "+"does not implement interface "+Class.name)},upgradeClass=function(Class){var ClassProto=Class.prototype,searchParams=gOPD(ClassProto,"searchParams"),href=gOPD(ClassProto,"href"),search=gOPD(ClassProto,"search"),createSearchParams;if(!searchParams&&search&&search.set){createSearchParams=createSearchParamsCreate(createSearchParamsPollute(search));Object.defineProperties(ClassProto,{href:{get:function(){return href.get.call(this)},set:function(value){var sp=this._searchParams;href.set.call(this,value);if(sp)updateSearchParams(sp)}},search:{get:function(){return search.get.call(this)},set:function(value){var sp=this._searchParams;search.set.call(this,value);if(sp)updateSearchParams(sp)}},searchParams:{get:function(){verifySearchParams(this,Class);return this._searchParams||createSearchParams(this,new URLSearchParams(this.search.slice(1)))},set:function(sp){verifySearchParams(this,Class);createSearchParams(this,sp)}}})}};upgradeClass(HTMLAnchorElement);if(/^function|object$/.test(typeof URL)&&URL.prototype)upgradeClass(URL);return URLSearchParams}();(function(URLSearchParamsProto){var iterable=function(){try{return!!Symbol.iterator}catch(error){return false}}();if(!("forEach"in URLSearchParamsProto)){URLSearchParamsProto.forEach=function forEach(callback,thisArg){var names=Object.create(null);this.toString().replace(/=[\s\S]*?(?:&|$)/g,"=").split("=").forEach(function(name){if(!name.length||name in names)return;(names[name]=this.getAll(name)).forEach(function(value){callback.call(thisArg,value,name,this)},this)},this)}}if(!("keys"in URLSearchParamsProto)){URLSearchParamsProto.keys=function keys(){var items=[];this.forEach(function(value,name){items.push(name)});var iterator={next:function(){var value=items.shift();return{done:value===undefined,value:value}}};if(iterable){iterator[Symbol.iterator]=function(){return iterator}}return iterator}}if(!("values"in URLSearchParamsProto)){URLSearchParamsProto.values=function values(){var items=[];this.forEach(function(value){items.push(value)});var iterator={next:function(){var value=items.shift();return{done:value===undefined,value:value}}};if(iterable){iterator[Symbol.iterator]=function(){return iterator}}return iterator}}if(!("entries"in URLSearchParamsProto)){URLSearchParamsProto.entries=function entries(){var items=[];this.forEach(function(value,name){items.push([name,value])});var iterator={next:function(){var value=items.shift();return{done:value===undefined,value:value}}};if(iterable){iterator[Symbol.iterator]=function(){return iterator}}return iterator}}if(iterable&&!(Symbol.iterator in URLSearchParamsProto)){URLSearchParamsProto[Symbol.iterator]=URLSearchParamsProto.entries}if(!("sort"in URLSearchParamsProto)){URLSearchParamsProto.sort=function sort(){var entries=this.entries(),entry=entries.next(),done=entry.done,keys=[],values=Object.create(null),i,key,value;while(!done){value=entry.value;key=value[0];keys.push(key);if(!(key in values)){values[key]=[]}values[key].push(value[1]);entry=entries.next();done=entry.done}keys.sort();for(i=0;i<keys.length;i++){this["delete"](keys[i])}for(i=0;i<keys.length;i++){key=keys[i];this.append(key,values[key].shift())}}}})(URLSearchParams.prototype);