From 7ca033726279f9d4030c644d34b1c6e2f86e9f34 Mon Sep 17 00:00:00 2001 From: alethes Date: Thu, 17 Apr 2014 07:20:38 +0200 Subject: [PATCH] Major overhaul: blaze, reactivity, fixes --- client/main.css | 173 ++++++++++++++++++++ lib/pages.coffee | 398 ++++++++++++++++++++++++++++++++++++++++++++++ public/loader.gif | Bin 0 -> 3208 bytes test/tests.coffee | 0 4 files changed, 571 insertions(+) create mode 100644 client/main.css create mode 100644 lib/pages.coffee create mode 100644 public/loader.gif create mode 100644 test/tests.coffee diff --git a/client/main.css b/client/main.css new file mode 100644 index 0000000..6eff909 --- /dev/null +++ b/client/main.css @@ -0,0 +1,173 @@ +.pagesItemDefault { + margin: 5px 20px; + padding: 10px; + border: 1px solid #999; + border-radius: 15px; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} + +.pagination > li { + display: inline; +} + +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.428571429; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; +} + +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; +} + +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + background-color: #eeeeee; +} + +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #ffffff; + cursor: default; + background-color: #428bca; + border-color: #428bca; +} + +.pagination > .disabled > span, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; + border-color: #dddddd; +} + +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} + +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; +} + +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} + +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; +} \ No newline at end of file diff --git a/lib/pages.coffee b/lib/pages.coffee new file mode 100644 index 0000000..082f7c7 --- /dev/null +++ b/lib/pages.coffee @@ -0,0 +1,398 @@ +@__Pages = class Pages + availableSettings: + dataMargin: [Number, 3] + filters: [Object, {}] + itemTemplate: [String, "_pagesItemDefault"] + navShowFirst: [Boolean, true] + navShowLast: [Boolean, true] + resetOnReload: [Boolean, false] + paginationMargin: [Number, 3] + perPage: [Number, 10] + requestTimeout: [Number, 2] + route: [String, "/page/"] + router: [true, false] #Can be any type. Use only in comparisons. Expects String or Boolean + routerTemplate: [String, "pages"] + sort: [Object, {}] + fields: [Object, {}] + #The following settings are unavailable to the client after initialization + infinite: false + infiniteItemsLimit: 30 + infiniteTrigger: .8 + infiniteRateLimit: 1 + itemTemplate: "_pagesItemDefault" + pageSizeLimit: 60 + rateLimit: 1 + homeRoute: "/" + templateName: false #Defaults to collection name + _ninstances: 0 + _ready: true + _bgready: true + _currentPage: 1 + collections: {} + instances: {} + subscriptions: [] + currentSubscription: null + methods: + "CountPages": -> + Math.ceil @Collection.find(@filters, + sort: @sort + ).count() / @perPage + "Set": (k, v = undefined) -> + if v? + changes = @set k, v, false, true + else + changes = 0 + for _k, _v of k + changes += @set _k, _v, false, true + changes + "Unsubscribe": -> + while @subscriptions.length + i = @subscriptions.shift() + continue unless i? + i.stop() + #s = i[0] + #for j in i[1] + # try + # s.removed @Collection._name, j + # catch e + # @log e + constructor: (collection, settings) -> + @setCollection collection + @setDefaults() + @applySettings settings + @setRouter() + @[(if Meteor.isServer then "server" else "client") + "Init"]() + @registerInstance() + @ + serverInit: -> + @setMethods() + self = @ + Meteor.publish @name, (page) -> + self.publish.call self, page, @ + clientInit: -> + @requested = [] + @received = [] + @queue = [] + @setTemplates() + @countPages() + @setInfiniteTrigger() if @infinite + @syncSettings ((err, changes) -> + @reload() if changes > 0 + ).bind @ + reload: -> + @unsubscribe (-> + @requested = [] + @received = [] + @queue = [] + @call "CountPages", ((e, total) -> + @sess "totalPages", total + p = @currentPage() + p = 1 if (not p?) or @resetOnReload or p > total + @sess "currentPage", false + @sess "currentPage", p + ).bind @ + ).bind @ + unsubscribe: (cb) -> + @call "Unsubscribe", (-> + cb() if cb? + ).bind(@) + setDefaults: -> + for k, v of @availableSettings + @[k] = v[1] if v[1]? + applySettings: (settings) -> + for key, value of settings + @set key, value, false, true + syncSettings: (cb) -> + S = {} + for k of @availableSettings + S[k] = @[k] + @set S, undefined, true, false, cb.bind @ + defaults: (k, v) -> + if v? + if typeof k is Object + _.map + else + Pages::[k] = v + else + Pages::[k] + setMethods: -> + nm = {} + for n, f of @methods + nm[@id + n] = f.bind @ + @methods = nm + Meteor.methods @methods + getMethod: (name)-> + @id + name + call: (method, cb) -> + Meteor.call @getMethod(method), cb.bind(@) + sess: (k, v) -> + k = "#{@id}.#{k}" + if v? + Session.set k, v + else + Session.get k + set: (k, v = undefined, onServer = true, init = false, cb) -> + if cb? + cb = cb.bind @ + else + cb = @reload.bind @ + if Meteor.isClient and onServer + Meteor.call @getMethod("Set"), k, v, cb + if v? + changes = @_set k, v, init + else + changes = 0 + for _k, _v of k + changes += @_set _k, _v, init + changes + _set: (k, v, init = false) -> + ch = 0 + if init or k of @availableSettings + if @availableSettings[k]? and @availableSettings[k][0] isnt true + check v, @availableSettings[k][0] + if JSON.stringify(@[k]) != JSON.stringify(v) + ch = 1 + @[k] = v + else + new Meteor.Error 400, "Setting not available." + ch + setId: (name) -> + if @templateName + name = @templateName + if name of Pages::instances + n = name.match /[0-9]+$/ + if n? + name = name[0 .. n[0].length] + parseInt(n) + 1 + else + name = name + "2" + @id = "pages_" + name + @name = name + registerInstance: -> + Pages::_ninstances++ + Pages::instances[@name] = @ + setCollection: (collection) -> + if typeof collection is 'object' + Pages::collections[collection._name] = collection + @Collection = collection + else + isNew = true + try + @Collection = new Meteor.Collection collection + Pages::collections[@name] = @Collection + catch e + isNew = false + @Collection = Pages::collections[@name] + @Collection or throw "The <#{collection}> collection + was defined outside of Pages. Pass the collection object + instead of collection name to the constructor." + @setId @Collection._name + @PaginatedCollection = new Meteor.Collection @id + setRouter: -> + if @router is "iron-router" + pr = "#{@route}:n" + t = @routerTemplate + self = @ + Router.map -> + if self.homeRoute + @route "home", + path: self.homeRoute + template: t + onBeforeAction: -> + self.sess "currentPage", 1 + unless self.infinite + @route "page", + path: pr + template: t + onBeforeAction: -> + self.onNavClick parseInt @params.n + setPerPage: -> + @perPage = if @pageSizeLimit < @perPage then @pageSizeLimit else @perPage + setTemplates: -> + name = if @templateName then @templateName else @name + Template[name].pagesData = @ + Template[name].pagesNav = Template['_pagesNav'] + Template[name].pages = Template['_pagesPage'] + countPages: -> + Meteor.call @getMethod("CountPages"), ((e, r) -> + @sess "totalPages", r + ).bind(@) + publish: (page, subscription) -> + @setPerPage() + skip = (page - 1) * @perPage + skip = 0 if skip < 0 + init = true + c = @Collection.find @filters, + sort: @sort + fields: @fields + skip: skip + limit: @perPage + self = @ + handle = @Collection.find().observeChanges + changed: ((subscription, id, fields) -> + subscription.changed @id, id, fields + ).bind @, subscription + added: ((subscription, id, fields) -> + subscription.added @id, id, fields unless init + ).bind @, subscription + removed: ((subscription, id) -> + subscription.removed @id, id + ).bind @, subscription + init = false + n = 0 + c.forEach ((doc, index, cursor) -> + n++ + doc["_#{@id}_p"] = page + doc["_#{@id}_i"] = index + subscription.added @id, doc._id, doc + ).bind @ + subscription.onStop -> + handle.stop() + @ready() + @subscriptions.push subscription + c + loading: (p) -> + @_bgready = false + @_ready = false + if p is @currentPage() and Session? + @sess "ready", false + now: -> + (new Date()).getTime() + log: (msg) -> + console.log "#{@name} #{msg}" + logRequest: (p) -> + @timeLastRequest = @now() + @loading p + @requested.push p unless p in @requested + logResponse: (p) -> + @received.push p unless p in @received + clearQueue: -> + @queue = [] + neighbors: (page) -> + @n = [page] + if @dataMargin is 0 + return @n + for d in [1 .. @dataMargin] + np = page + d + if np <= @sess "totalPages" + @n.push np + pp = page - d + if pp > 0 + @n.push pp + @n + paginationNavItem: (label, page, disabled, active = false) -> + p: label + n: page + active: if active then "active" else "" + disabled: if disabled then "disabled" else "" + paginationNeighbors: -> + page = @currentPage() + total = @sess "totalPages" + from = page - @paginationMargin + to = page + @paginationMargin + if from < 1 + to += 1 - from + from = 1 + if to > total + from -= to - total + to = total + from = 1 if from < 1 + to = total if to > total + n = [] + if @navShowFirst + n.push @paginationNavItem "«", 1, page == 1 + n.push @paginationNavItem "<", page - 1, page == 1 + for p in [from .. to] + n.push @paginationNavItem p, p, page > total, p is page + n.push @paginationNavItem ">", page + 1, page >= total + if @navShowLast + n.push @paginationNavItem "»", total, page >= total + for i, k in n + n[k]['_p'] = @ + n + onNavClick: (n) -> + if n <= @sess("totalPages") and n > 0 + Deps.nonreactive (-> + @sess "oldPage", @sess "currentPage" + ).bind @ + @sess "currentPage", n + setInfiniteTrigger: -> + window.onscroll = (_.throttle -> + t = @infiniteTrigger + oh = document.body.offsetHeight + if t > 1 + l = oh - t + else if t > 0 + l = oh * t + else + return + if (window.innerHeight + window.scrollY) >= l + if @lastPage < @sess "totalPages" + @sess("currentPage", @lastPage + 1) + , @infiniteRateLimit * 1000 + ).bind @ + checkQueue: -> + if @queue.length + i = @queue.shift() until i in @neighbors(@currentPage()) or not @queue.length + @requestPage i if i in @neighbors @currentPage() + currentPage: -> + if Meteor.isClient and @sess("currentPage")? + @sess "currentPage" + else + @_currentPage + isReady: -> + @sess "ready" + ready: (p) -> + @_ready = true + if p == true or p is @currentPage() and Session? + @sess "ready", true + + getPage: (page) -> + page = @currentPage() unless page? + page = parseInt(page) + return if page is NaN + if Meteor.isClient + if page <= @sess "totalPages" + for p in @neighbors page + @requestPage p unless p in @received + if @infinite + n = @PaginatedCollection.find({}, + fields: @fields + sort: @sort + ).count() + c = @PaginatedCollection.find({}, + fields: @fields + sort: @sort + skip: if @infiniteItemsLimit isnt Infinity and n > @infiniteItemsLimit then n - @infiniteItemsLimit else 0 + limit: @infiniteItemsLimit + ).fetch() + else if page in @received + c = @PaginatedCollection.find( + _.object([ + ["_#{@id}_p", page] + ]), + fields: @fields + #sort: @sort + ).fetch() + c + requestPage: (page) -> + return if page in @requested + @clearQueue() if page is @currentPage() + @queue.push page + @logRequest page + Meteor.defer ((page) -> + @subscriptions[page] = Meteor.subscribe @name, page, + onReady: ((page) -> + @onPage page + ).bind @, page + onError: ((e) -> + new Meteor.Error e.message + ).bind @ + ).bind @, page + onPage: (page) -> + @logResponse page + @ready page + if @infinite + @lastPage = page + @checkQueue() + +Meteor.Pagination = Pages \ No newline at end of file diff --git a/public/loader.gif b/public/loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..3c2f7c058836808f7f1f5f9a4bdf37d4e5f9284a GIT binary patch literal 3208 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