From 95ddb61123c15733e591ba1bf20e966d6bab25cd Mon Sep 17 00:00:00 2001 From: Ohyeon Date: Fri, 24 Apr 2020 21:07:42 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A3=A8=EB=A8=B8=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=9D=84=20=EC=A7=80=EC=9A=B0=EA=B3=A0,=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/javascripts/application.js | 14 +- app/assets/javascripts/data_sets.coffee | 3 - app/assets/javascripts/pages.coffee | 3 - app/assets/javascripts/parti.js | 20 + app/assets/stylesheets/parti.scss | 11 + app/controllers/links_controller.rb | 5 + app/views/application/_aside.html.haml | 9 +- app/views/application/_header.html.haml | 14 +- app/views/application/_search_bar.html.haml | 6 - app/views/data_sets/index.html.haml | 4 +- app/views/layouts/application.html.haml | 15 +- app/views/links/_link.html.haml | 6 + app/views/links/index.html.haml | 10 + config/locales/ko.yml | 8 +- config/routes.rb | 2 +- vendor/javascripts/infinite-scroll.pkgd.js | 2222 +++++++++++++++++++ 16 files changed, 2296 insertions(+), 56 deletions(-) delete mode 100644 app/assets/javascripts/data_sets.coffee delete mode 100644 app/assets/javascripts/pages.coffee create mode 100644 app/assets/javascripts/parti.js create mode 100644 app/controllers/links_controller.rb delete mode 100644 app/views/application/_search_bar.html.haml create mode 100644 app/views/links/_link.html.haml create mode 100644 app/views/links/index.html.haml create mode 100644 vendor/javascripts/infinite-scroll.pkgd.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c3d7fcc..c9c45f8 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,16 +14,4 @@ //= require summernote/summernote-bs4.min //= require summernote-slowalk //= require turbolinks - -$(document).on('ready show.bs closed.bs load page:change turbolinks:load', function () { - $.onmount(); - initializeSummernote(); - - $('[data-toggle="offcanvas"]').on('click', function () { - $('.offcanvas-collapse').toggleClass('open'); - }) -}); - -$(document).on('turbolinks:before-cache', function () { $.onmount.teardown() }); - -$.onmount('.js-tooltip', function () { $(this).tooltip() }); \ No newline at end of file +//= require parti \ No newline at end of file diff --git a/app/assets/javascripts/data_sets.coffee b/app/assets/javascripts/data_sets.coffee deleted file mode 100644 index 24f83d1..0000000 --- a/app/assets/javascripts/data_sets.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/pages.coffee b/app/assets/javascripts/pages.coffee deleted file mode 100644 index 24f83d1..0000000 --- a/app/assets/javascripts/pages.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/parti.js b/app/assets/javascripts/parti.js new file mode 100644 index 0000000..8922fa8 --- /dev/null +++ b/app/assets/javascripts/parti.js @@ -0,0 +1,20 @@ +$(document).on('ready show.bs closed.bs load page:change turbolinks:load', function () { + $.onmount(); + + initializeSummernote(); + + $('[data-toggle="offcanvas"]').on('click', function () { + $('.offcanvas-collapse').toggleClass('open'); + }) + + $('.infinite-scroll').infiniteScroll({ + path: ".pagination a[rel=next]", + append: ".infinite-scroll-item", + prefill: true, + debug: true + }); +}); + +$(document).on('turbolinks:before-cache', function () { $.onmount.teardown() }); + +$.onmount('.js-tooltip', function () { $(this).tooltip() }); \ No newline at end of file diff --git a/app/assets/stylesheets/parti.scss b/app/assets/stylesheets/parti.scss index 634cfa7..a2e452d 100644 --- a/app/assets/stylesheets/parti.scss +++ b/app/assets/stylesheets/parti.scss @@ -104,6 +104,17 @@ header nav.navbar.fixed-top { left: 0; } +.card .card-image { + position: relative; + padding-bottom: 56.2%; + img { + position:absolute; + width: 100%; + height: 100%; + object-fit: cover; + } +} + .card-columns { @include media-breakpoint-only(sm) { column-count: 2; diff --git a/app/controllers/links_controller.rb b/app/controllers/links_controller.rb new file mode 100644 index 0000000..919e954 --- /dev/null +++ b/app/controllers/links_controller.rb @@ -0,0 +1,5 @@ +class LinksController < ApplicationController + def index + @links = Link.order("id desc").page(params[:page]) + end +end \ No newline at end of file diff --git a/app/views/application/_aside.html.haml b/app/views/application/_aside.html.haml index 49ef573..ad561f8 100644 --- a/app/views/application/_aside.html.haml +++ b/app/views/application/_aside.html.haml @@ -1,5 +1,3 @@ -= render "search_bar" - %section %h5.text-muted= t('menu.tags') .mb-4 @@ -15,6 +13,13 @@ = link_to data_set, class: "list-group-item list-group-item-action" do %h6.my-0= data_set.title +%section + %h5.text-muted 최신 관련자료 + .mb-4.list-group + - Link.limit(20).each do |link| + = link_to link.data_set, class: "list-group-item list-group-item-action" do + %h6.my-0= link.title + %section .mb-4 .border.p-3 diff --git a/app/views/application/_header.html.haml b/app/views/application/_header.html.haml index ec02dfa..f9a5791 100644 --- a/app/views/application/_header.html.haml +++ b/app/views/application/_header.html.haml @@ -10,13 +10,7 @@ %small.text-light %i.far.fa-database %li.nav-item= link_to t("menu.data_sets"), data_sets_path, class: "nav-link" - - if user_signed_in? - %li.nav-item= link_to t("link.new_data_set"), new_data_set_path, class: "nav-link" - - %li.nav-item.mt-4 - %small.text-light - %i.far.fa-wind - %li.nav-item= link_to t("menu.rumors"), rumors_path, class: "nav-link" + %li.nav-item= link_to t("menu.links"), links_path, class: "nav-link" %li.nav-item.mt-4 %small.text-light @@ -29,4 +23,8 @@ - else %li.nav-item= link_to t("link.sign_in"), new_user_session_path, class: "nav-link" - = link_to t("site_name"), root_path, class: "navbar-brand" \ No newline at end of file + .mr-auto + = link_to t("site_name"), root_path, class: "navbar-brand" + + = form_with url: data_sets_path, method: :get, local: true, class: "form-inline" do |f| + = f.text_field :q, value: params[:q], placeholder: "검색", class: "form-control form-control-sm" diff --git a/app/views/application/_search_bar.html.haml b/app/views/application/_search_bar.html.haml deleted file mode 100644 index c64ef17..0000000 --- a/app/views/application/_search_bar.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%section - %h5.text-muted= t('menu.search') - .bg-white.border.p-4.mb-4 - = form_with url: data_sets_path, method: :get, local: true do |f| - = f.text_field :q, value: params[:q], class: "form-control mb-2" - = f.submit t('btn.search'), class: "btn btn-outline-primary btn-sm" \ No newline at end of file diff --git a/app/views/data_sets/index.html.haml b/app/views/data_sets/index.html.haml index 71e2671..277bc54 100644 --- a/app/views/data_sets/index.html.haml +++ b/app/views/data_sets/index.html.haml @@ -1,7 +1,9 @@ %h2.pb-2 - = t('main.title') + = t('menu.data_sets') - if params[:q].present? = "'#{params[:q]}'" + %small + = link_to t('menu.links'), links_path, class: "text-muted" .row - @data_sets.each do |data_set| diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 6921f27..52f61a8 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,7 +4,7 @@ %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"} %meta{:charset => "utf-8"} %meta{:content => "width=device-width, initial-scale=1, shrink-to-fit=no", :name => "viewport"} - %title 데이터퍼블릭 + %title 빠띠 데이터퍼블릭 = csrf_meta_tags = csp_meta_tag = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' @@ -13,18 +13,7 @@ %body.bg-light = render "header" - - if current_page?(root_url) - %header - .container - .py-5.text-center - %span.display-3.bg-dark.text-white.rounded.px-3.font-weight-bold D - %h2.mt-4 - %span.font-weight-bold 데이터퍼블릭 - %small.font-weight-light .kr - %p.p-2.lead.text-muted - 사회적으로 이슈가 된 사안들의 원본 데이터들을 출처와 함께 모읍니다. - - %main.py-4 + %main.py-5 .container .row .col-md-8 diff --git a/app/views/links/_link.html.haml b/app/views/links/_link.html.haml new file mode 100644 index 0000000..51e6bcc --- /dev/null +++ b/app/views/links/_link.html.haml @@ -0,0 +1,6 @@ +.list-group-item[link] + %h5.mb-1 + = link_to link.url, class: "text-dark" do + = link.title + + %p.mb-1= link_to link.data_set.title, link.data_set, class: "text-muted" \ No newline at end of file diff --git a/app/views/links/index.html.haml b/app/views/links/index.html.haml new file mode 100644 index 0000000..859e6b0 --- /dev/null +++ b/app/views/links/index.html.haml @@ -0,0 +1,10 @@ +%h2.pb-2 + = t('menu.links') + %small + = link_to t('menu.data_sets'), data_sets_path, class: "text-muted" + +.list-group + = render @links + +.d-flex.justify-content-center + = paginate @links \ No newline at end of file diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 3c798b2..553d5ec 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -22,19 +22,15 @@ ko: tags: 태그 links: 관련정보 comments: 댓글 - rumor: - title: 제목 - body: 내용 - comments: 댓글 link: title: 제목 body: 요약 url: URL - site_name: 데이터퍼블릭.kr + site_name: 빠띠 데이터퍼블릭 menu: data_sets: 원 데이터 - rumors: 루머 + links: 관련자료 fact_checks: 팩트체크 tags: 태그 search: 검색 diff --git a/config/routes.rb b/config/routes.rb index 5e76a6f..6ce0eb2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,8 +2,8 @@ devise_for :users resources :data_sets - resources :rumors resources :tags + resources :links root "data_sets#index" end diff --git a/vendor/javascripts/infinite-scroll.pkgd.js b/vendor/javascripts/infinite-scroll.pkgd.js new file mode 100644 index 0000000..3904d45 --- /dev/null +++ b/vendor/javascripts/infinite-scroll.pkgd.js @@ -0,0 +1,2222 @@ +/*! + * Infinite Scroll PACKAGED v3.0.6 + * Automatically add next page + * + * Licensed GPLv3 for open source use + * or Infinite Scroll Commercial License for commercial use + * + * https://infinite-scroll.com + * Copyright 2018 Metafizzy + */ + +/** + * Bridget makes jQuery widgets + * v2.0.1 + * MIT license + */ + +/* jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + // universal module definition + /*jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'jquery-bridget/jquery-bridget',[ 'jquery' ], function( jQuery ) { + return factory( window, jQuery ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('jquery') + ); + } else { + // browser global + window.jQueryBridget = factory( + window, + window.jQuery + ); + } + +}( window, function factory( window, jQuery ) { +'use strict'; + +// ----- utils ----- // + +var arraySlice = Array.prototype.slice; + +// helper function for logging errors +// $.error breaks jQuery chaining +var console = window.console; +var logError = typeof console == 'undefined' ? function() {} : + function( message ) { + console.error( message ); + }; + +// ----- jQueryBridget ----- // + +function jQueryBridget( namespace, PluginClass, $ ) { + $ = $ || jQuery || window.jQuery; + if ( !$ ) { + return; + } + + // add option method -> $().plugin('option', {...}) + if ( !PluginClass.prototype.option ) { + // option setter + PluginClass.prototype.option = function( opts ) { + // bail out if not an object + if ( !$.isPlainObject( opts ) ){ + return; + } + this.options = $.extend( true, this.options, opts ); + }; + } + + // make jQuery plugin + $.fn[ namespace ] = function( arg0 /*, arg1 */ ) { + if ( typeof arg0 == 'string' ) { + // method call $().plugin( 'methodName', { options } ) + // shift arguments by 1 + var args = arraySlice.call( arguments, 1 ); + return methodCall( this, arg0, args ); + } + // just $().plugin({ options }) + plainCall( this, arg0 ); + return this; + }; + + // $().plugin('methodName') + function methodCall( $elems, methodName, args ) { + var returnValue; + var pluginMethodStr = '$().' + namespace + '("' + methodName + '")'; + + $elems.each( function( i, elem ) { + // get instance + var instance = $.data( elem, namespace ); + if ( !instance ) { + logError( namespace + ' not initialized. Cannot call methods, i.e. ' + + pluginMethodStr ); + return; + } + + var method = instance[ methodName ]; + if ( !method || methodName.charAt(0) == '_' ) { + logError( pluginMethodStr + ' is not a valid method' ); + return; + } + + // apply method, get return value + var value = method.apply( instance, args ); + // set return value if value is returned, use only first value + returnValue = returnValue === undefined ? value : returnValue; + }); + + return returnValue !== undefined ? returnValue : $elems; + } + + function plainCall( $elems, options ) { + $elems.each( function( i, elem ) { + var instance = $.data( elem, namespace ); + if ( instance ) { + // set options & init + instance.option( options ); + instance._init(); + } else { + // initialize new instance + instance = new PluginClass( elem, options ); + $.data( elem, namespace, instance ); + } + }); + } + + updateJQuery( $ ); + +} + +// ----- updateJQuery ----- // + +// set $.bridget for v1 backwards compatibility +function updateJQuery( $ ) { + if ( !$ || ( $ && $.bridget ) ) { + return; + } + $.bridget = jQueryBridget; +} + +updateJQuery( jQuery || window.jQuery ); + +// ----- ----- // + +return jQueryBridget; + +})); + +/** + * EvEmitter v1.1.0 + * Lil' event emitter + * MIT License + */ + +/* jshint unused: true, undef: true, strict: true */ + +( function( global, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, window */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'ev-emitter/ev-emitter',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory(); + } else { + // Browser globals + global.EvEmitter = factory(); + } + +}( typeof window != 'undefined' ? window : this, function() { + + + +function EvEmitter() {} + +var proto = EvEmitter.prototype; + +proto.on = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // set events hash + var events = this._events = this._events || {}; + // set listeners array + var listeners = events[ eventName ] = events[ eventName ] || []; + // only add once + if ( listeners.indexOf( listener ) == -1 ) { + listeners.push( listener ); + } + + return this; +}; + +proto.once = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // add event + this.on( eventName, listener ); + // set once flag + // set onceEvents hash + var onceEvents = this._onceEvents = this._onceEvents || {}; + // set onceListeners object + var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {}; + // set flag + onceListeners[ listener ] = true; + + return this; +}; + +proto.off = function( eventName, listener ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var index = listeners.indexOf( listener ); + if ( index != -1 ) { + listeners.splice( index, 1 ); + } + + return this; +}; + +proto.emitEvent = function( eventName, args ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + // copy over to avoid interference if .off() in listener + listeners = listeners.slice(0); + args = args || []; + // once stuff + var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; + + for ( var i=0; i < listeners.length; i++ ) { + var listener = listeners[i] + var isOnce = onceListeners && onceListeners[ listener ]; + if ( isOnce ) { + // remove listener + // remove before trigger to prevent recursion + this.off( eventName, listener ); + // unset once flag + delete onceListeners[ listener ]; + } + // trigger listener + listener.apply( this, args ); + } + + return this; +}; + +proto.allOff = function() { + delete this._events; + delete this._onceEvents; +}; + +return EvEmitter; + +})); + +/** + * matchesSelector v2.0.2 + * matchesSelector( element, '.selector' ) + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + /*global define: false, module: false */ + 'use strict'; + // universal module definition + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'desandro-matches-selector/matches-selector',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.matchesSelector = factory(); + } + +}( window, function factory() { + 'use strict'; + + var matchesMethod = ( function() { + var ElemProto = window.Element.prototype; + // check for the standard method name first + if ( ElemProto.matches ) { + return 'matches'; + } + // check un-prefixed + if ( ElemProto.matchesSelector ) { + return 'matchesSelector'; + } + // check vendor prefixes + var prefixes = [ 'webkit', 'moz', 'ms', 'o' ]; + + for ( var i=0; i < prefixes.length; i++ ) { + var prefix = prefixes[i]; + var method = prefix + 'MatchesSelector'; + if ( ElemProto[ method ] ) { + return method; + } + } + })(); + + return function matchesSelector( elem, selector ) { + return elem[ matchesMethod ]( selector ); + }; + +})); + +/** + * Fizzy UI utils v2.0.7 + * MIT license + */ + +/*jshint browser: true, undef: true, unused: true, strict: true */ + +( function( window, factory ) { + // universal module definition + /*jshint strict: false */ /*globals define, module, require */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'fizzy-ui-utils/utils',[ + 'desandro-matches-selector/matches-selector' + ], function( matchesSelector ) { + return factory( window, matchesSelector ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('desandro-matches-selector') + ); + } else { + // browser global + window.fizzyUIUtils = factory( + window, + window.matchesSelector + ); + } + +}( window, function factory( window, matchesSelector ) { + + + +var utils = {}; + +// ----- extend ----- // + +// extends objects +utils.extend = function( a, b ) { + for ( var prop in b ) { + a[ prop ] = b[ prop ]; + } + return a; +}; + +// ----- modulo ----- // + +utils.modulo = function( num, div ) { + return ( ( num % div ) + div ) % div; +}; + +// ----- makeArray ----- // + +var arraySlice = Array.prototype.slice; + +// turn element or nodeList into an array +utils.makeArray = function( obj ) { + if ( Array.isArray( obj ) ) { + // use object if already an array + return obj; + } + // return empty array if undefined or null. #6 + if ( obj === null || obj === undefined ) { + return []; + } + + var isArrayLike = typeof obj == 'object' && typeof obj.length == 'number'; + if ( isArrayLike ) { + // convert nodeList to array + return arraySlice.call( obj ); + } + + // array of single index + return [ obj ]; +}; + +// ----- removeFrom ----- // + +utils.removeFrom = function( ary, obj ) { + var index = ary.indexOf( obj ); + if ( index != -1 ) { + ary.splice( index, 1 ); + } +}; + +// ----- getParent ----- // + +utils.getParent = function( elem, selector ) { + while ( elem.parentNode && elem != document.body ) { + elem = elem.parentNode; + if ( matchesSelector( elem, selector ) ) { + return elem; + } + } +}; + +// ----- getQueryElement ----- // + +// use element as selector string +utils.getQueryElement = function( elem ) { + if ( typeof elem == 'string' ) { + return document.querySelector( elem ); + } + return elem; +}; + +// ----- handleEvent ----- // + +// enable .ontype to trigger from .addEventListener( elem, 'type' ) +utils.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +// ----- filterFindElements ----- // + +utils.filterFindElements = function( elems, selector ) { + // make array of elems + elems = utils.makeArray( elems ); + var ffElems = []; + + elems.forEach( function( elem ) { + // check that elem is an actual element + if ( !( elem instanceof HTMLElement ) ) { + return; + } + // add elem if no selector + if ( !selector ) { + ffElems.push( elem ); + return; + } + // filter & find items if we have a selector + // filter + if ( matchesSelector( elem, selector ) ) { + ffElems.push( elem ); + } + // find children + var childElems = elem.querySelectorAll( selector ); + // concat childElems to filterFound array + for ( var i=0; i < childElems.length; i++ ) { + ffElems.push( childElems[i] ); + } + }); + + return ffElems; +}; + +// ----- debounceMethod ----- // + +utils.debounceMethod = function( _class, methodName, threshold ) { + threshold = threshold || 100; + // original method + var method = _class.prototype[ methodName ]; + var timeoutName = methodName + 'Timeout'; + + _class.prototype[ methodName ] = function() { + var timeout = this[ timeoutName ]; + clearTimeout( timeout ); + + var args = arguments; + var _this = this; + this[ timeoutName ] = setTimeout( function() { + method.apply( _this, args ); + delete _this[ timeoutName ]; + }, threshold ); + }; +}; + +// ----- docReady ----- // + +utils.docReady = function( callback ) { + var readyState = document.readyState; + if ( readyState == 'complete' || readyState == 'interactive' ) { + // do async to allow for other scripts to run. metafizzy/flickity#441 + setTimeout( callback ); + } else { + document.addEventListener( 'DOMContentLoaded', callback ); + } +}; + +// ----- htmlInit ----- // + +// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/ +utils.toDashed = function( str ) { + return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) { + return $1 + '-' + $2; + }).toLowerCase(); +}; + +var console = window.console; +/** + * allow user to initialize classes via [data-namespace] or .js-namespace class + * htmlInit( Widget, 'widgetName' ) + * options are parsed from data-namespace-options + */ +utils.htmlInit = function( WidgetClass, namespace ) { + utils.docReady( function() { + var dashedNamespace = utils.toDashed( namespace ); + var dataAttr = 'data-' + dashedNamespace; + var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' ); + var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace ); + var elems = utils.makeArray( dataAttrElems ) + .concat( utils.makeArray( jsDashElems ) ); + var dataOptionsAttr = dataAttr + '-options'; + var jQuery = window.jQuery; + + elems.forEach( function( elem ) { + var attr = elem.getAttribute( dataAttr ) || + elem.getAttribute( dataOptionsAttr ); + var options; + try { + options = attr && JSON.parse( attr ); + } catch ( error ) { + // log error, do not initialize + if ( console ) { + console.error( 'Error parsing ' + dataAttr + ' on ' + elem.className + + ': ' + error ); + } + return; + } + // initialize + var instance = new WidgetClass( elem, options ); + // make available via $().data('namespace') + if ( jQuery ) { + jQuery.data( elem, namespace, instance ); + } + }); + + }); +}; + +// ----- ----- // + +return utils; + +})); + +// core +( function( window, factory ) { + // universal module definition + /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'infinite-scroll/js/core',[ + 'ev-emitter/ev-emitter', + 'fizzy-ui-utils/utils', + ], function( EvEmitter, utils) { + return factory( window, EvEmitter, utils ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('ev-emitter'), + require('fizzy-ui-utils') + ); + } else { + // browser global + window.InfiniteScroll = factory( + window, + window.EvEmitter, + window.fizzyUIUtils + ); + } + +}( window, function factory( window, EvEmitter, utils ) { + +var jQuery = window.jQuery; +// internal store of all InfiniteScroll intances +var instances = {}; + +function InfiniteScroll( element, options ) { + var queryElem = utils.getQueryElement( element ); + + if ( !queryElem ) { + console.error( 'Bad element for InfiniteScroll: ' + ( queryElem || element ) ); + return; + } + element = queryElem; + // do not initialize twice on same element + if ( element.infiniteScrollGUID ) { + var instance = instances[ element.infiniteScrollGUID ]; + instance.option( options ); + return instance; + } + + this.element = element; + // options + this.options = utils.extend( {}, InfiniteScroll.defaults ); + this.option( options ); + // add jQuery + if ( jQuery ) { + this.$element = jQuery( this.element ); + } + + this.create(); +} + +// defaults +InfiniteScroll.defaults = { + // path: null, + // hideNav: null, + // debug: false, +}; + +// create & destroy methods +InfiniteScroll.create = {}; +InfiniteScroll.destroy = {}; + +var proto = InfiniteScroll.prototype; +// inherit EvEmitter +utils.extend( proto, EvEmitter.prototype ); + +// -------------------------- -------------------------- // + +// globally unique identifiers +var GUID = 0; + +proto.create = function() { + // create core + // add id for InfiniteScroll.data + var id = this.guid = ++GUID; + this.element.infiniteScrollGUID = id; // expando + instances[ id ] = this; // associate via id + // properties + this.pageIndex = 1; // default to first page + this.loadCount = 0; + this.updateGetPath(); + // bail if getPath not set, or returns falsey #776 + var hasPath = this.getPath && this.getPath(); + if ( !hasPath ) { + console.error('Disabling InfiniteScroll'); + return; + } + this.updateGetAbsolutePath(); + this.log( 'initialized', [ this.element.className ] ); + this.callOnInit(); + // create features + for ( var method in InfiniteScroll.create ) { + InfiniteScroll.create[ method ].call( this ); + } +}; + +proto.option = function( opts ) { + utils.extend( this.options, opts ); +}; + +// call onInit option, used for binding events on init +proto.callOnInit = function() { + var onInit = this.options.onInit; + if ( onInit ) { + onInit.call( this, this ); + } +}; + +// ----- events ----- // + +proto.dispatchEvent = function( type, event, args ) { + this.log( type, args ); + var emitArgs = event ? [ event ].concat( args ) : args; + this.emitEvent( type, emitArgs ); + // trigger jQuery event + if ( !jQuery || !this.$element ) { + return; + } + // namespace jQuery event + type += '.infiniteScroll'; + var $event = type; + if ( event ) { + // create jQuery event + var jQEvent = jQuery.Event( event ); + jQEvent.type = type; + $event = jQEvent; + } + this.$element.trigger( $event, args ); +}; + +var loggers = { + initialized: function( className ) { + return 'on ' + className; + }, + request: function( path ) { + return 'URL: ' + path; + }, + load: function( response, path ) { + return ( response.title || '' ) + '. URL: ' + path; + }, + error: function( error, path ) { + return error + '. URL: ' + path; + }, + append: function( response, path, items ) { + return items.length + ' items. URL: ' + path; + }, + last: function( response, path ) { + return 'URL: ' + path; + }, + history: function( title, path ) { + return 'URL: ' + path; + }, + pageIndex: function( index, origin ) { + return 'current page determined to be: ' + index + ' from ' + origin; + }, +}; + +// log events +proto.log = function( type, args ) { + if ( !this.options.debug ) { + return; + } + var message = '[InfiniteScroll] ' + type; + var logger = loggers[ type ]; + if ( logger ) { + message += '. ' + logger.apply( this, args ); + } + console.log( message ); +}; + +// -------------------------- methods used amoung features -------------------------- // + +proto.updateMeasurements = function() { + this.windowHeight = window.innerHeight; + var rect = this.element.getBoundingClientRect(); + this.top = rect.top + window.pageYOffset; +}; + +proto.updateScroller = function() { + var elementScroll = this.options.elementScroll; + if ( !elementScroll ) { + // default, use window + this.scroller = window; + return; + } + // if true, set to element, otherwise use option + this.scroller = elementScroll === true ? this.element : + utils.getQueryElement( elementScroll ); + if ( !this.scroller ) { + throw 'Unable to find elementScroll: ' + elementScroll; + } +}; + +// -------------------------- page path -------------------------- // + +proto.updateGetPath = function() { + var optPath = this.options.path; + if ( !optPath ) { + console.error( 'InfiniteScroll path option required. Set as: ' + optPath ); + return; + } + // function + var type = typeof optPath; + if ( type == 'function' ) { + this.getPath = optPath; + return; + } + // template string: '/pages/{{#}}.html' + var templateMatch = type == 'string' && optPath.match('{{#}}'); + if ( templateMatch ) { + this.updateGetPathTemplate( optPath ); + return; + } + // selector: '.next-page-selector' + this.updateGetPathSelector( optPath ); +}; + +proto.updateGetPathTemplate = function( optPath ) { + // set getPath with template string + this.getPath = function() { + var nextIndex = this.pageIndex + 1; + return optPath.replace( '{{#}}', nextIndex ); + }.bind( this ); + // get pageIndex from location + // convert path option into regex to look for pattern in location + // escape query (?) in url, allows for parsing GET parameters + var regexString = optPath + .replace( /(\\\?|\?)/, '\\?' ) + .replace( '{{#}}', '(\\d\\d?\\d?)' ); + var templateRe = new RegExp( regexString ); + var match = location.href.match( templateRe ); + + if ( match ) { + this.pageIndex = parseInt( match[1], 10 ); + this.log( 'pageIndex', [ this.pageIndex, 'template string' ] ); + } +}; + +var pathRegexes = [ + // WordPress & Tumblr - example.com/page/2 + // Jekyll - example.com/page2 + /^(.*?\/?page\/?)(\d\d?\d?)(.*?$)/, + // Drupal - example.com/?page=1 + /^(.*?\/?\?page=)(\d\d?\d?)(.*?$)/, + // catch all, last occurence of a number + /(.*?)(\d\d?\d?)(?!.*\d)(.*?$)/, +]; + +proto.updateGetPathSelector = function( optPath ) { + // parse href of link: '.next-page-link' + var hrefElem = document.querySelector( optPath ); + if ( !hrefElem ) { + console.error( 'Bad InfiniteScroll path option. Next link not found: ' + + optPath ); + return; + } + var href = hrefElem.getAttribute('href'); + // try matching href to pathRegexes patterns + var pathParts, regex; + for ( var i=0; href && i < pathRegexes.length; i++ ) { + regex = pathRegexes[i]; + var match = href.match( regex ); + if ( match ) { + pathParts = match.slice(1); // remove first part + break; + } + } + if ( !pathParts ) { + console.error( 'InfiniteScroll unable to parse next link href: ' + href ); + return; + } + this.isPathSelector = true; // flag for checkLastPage() + this.getPath = function() { + var nextIndex = this.pageIndex + 1; + return pathParts[0] + nextIndex + pathParts[2]; + }.bind( this ); + // get pageIndex from href + this.pageIndex = parseInt( pathParts[1], 10 ) - 1; + this.log( 'pageIndex', [ this.pageIndex, 'next link' ] ); +}; + +proto.updateGetAbsolutePath = function() { + var path = this.getPath(); + // path doesn't start with http or / + var isAbsolute = path.match( /^http/ ) || path.match( /^\// ); + if ( isAbsolute ) { + this.getAbsolutePath = this.getPath; + return; + } + + var pathname = location.pathname; + // query parameter #829. example.com/?pg=2 + var isQuery = path.match( /^\?/ ); + if ( isQuery ) { + this.getAbsolutePath = function() { + return pathname + this.getPath(); + }; + return; + } + + // /foo/bar/index.html => /foo/bar + var directory = pathname.substring( 0, pathname.lastIndexOf('/') ); + this.getAbsolutePath = function() { + return directory + '/' + this.getPath(); + }; +}; + +// -------------------------- nav -------------------------- // + +// hide navigation +InfiniteScroll.create.hideNav = function() { + var nav = utils.getQueryElement( this.options.hideNav ); + if ( !nav ) { + return; + } + nav.style.display = 'none'; + this.nav = nav; +}; + +InfiniteScroll.destroy.hideNav = function() { + if ( this.nav ) { + this.nav.style.display = ''; + } +}; + +// -------------------------- destroy -------------------------- // + +proto.destroy = function() { + this.allOff(); // remove all event listeners + // call destroy methods + for ( var method in InfiniteScroll.destroy ) { + InfiniteScroll.destroy[ method ].call( this ); + } + + delete this.element.infiniteScrollGUID; + delete instances[ this.guid ]; + // remove jQuery data. #807 + if ( jQuery && this.$element ) { + jQuery.removeData( this.element, 'infiniteScroll' ); + } +}; + +// -------------------------- utilities -------------------------- // + +// https://remysharp.com/2010/07/21/throttling-function-calls +InfiniteScroll.throttle = function( fn, threshold ) { + threshold = threshold || 200; + var last, timeout; + + return function() { + var now = +new Date(); + var args = arguments; + var trigger = function() { + last = now; + fn.apply( this, args ); + }.bind( this ); + if ( last && now < last + threshold ) { + // hold on to it + clearTimeout( timeout ); + timeout = setTimeout( trigger, threshold ); + } else { + trigger(); + } + }; +}; + +InfiniteScroll.data = function( elem ) { + elem = utils.getQueryElement( elem ); + var id = elem && elem.infiniteScrollGUID; + return id && instances[ id ]; +}; + +// set internal jQuery, for Webpack + jQuery v3 +InfiniteScroll.setJQuery = function( $ ) { + jQuery = $; +}; + +// -------------------------- setup -------------------------- // + +utils.htmlInit( InfiniteScroll, 'infinite-scroll' ); + +// add noop _init method for jQuery Bridget. #768 +proto._init = function() {}; + +if ( jQuery && jQuery.bridget ) { + jQuery.bridget( 'infiniteScroll', InfiniteScroll ); +} + +// -------------------------- -------------------------- // + +return InfiniteScroll; + +})); + +// page-load +( function( window, factory ) { + // universal module definition + /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'infinite-scroll/js/page-load',[ + './core', + ], function( InfiniteScroll ) { + return factory( window, InfiniteScroll ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('./core') + ); + } else { + // browser global + factory( + window, + window.InfiniteScroll + ); + } + +}( window, function factory( window, InfiniteScroll ) { + +var proto = InfiniteScroll.prototype; + +// InfiniteScroll.defaults.append = false; +InfiniteScroll.defaults.loadOnScroll = true; +InfiniteScroll.defaults.checkLastPage = true; +InfiniteScroll.defaults.responseType = 'document'; +// InfiniteScroll.defaults.prefill = false; +// InfiniteScroll.defaults.outlayer = null; + +InfiniteScroll.create.pageLoad = function() { + this.canLoad = true; + this.on( 'scrollThreshold', this.onScrollThresholdLoad ); + this.on( 'load', this.checkLastPage ); + if ( this.options.outlayer ) { + this.on( 'append', this.onAppendOutlayer ); + } +}; + +proto.onScrollThresholdLoad = function() { + if ( this.options.loadOnScroll ) { + this.loadNextPage(); + } +}; + +proto.loadNextPage = function() { + if ( this.isLoading || !this.canLoad ) { + return; + } + + var path = this.getAbsolutePath(); + this.isLoading = true; + + var onLoad = function( response ) { + this.onPageLoad( response, path ); + }.bind( this ); + + var onError = function( error ) { + this.onPageError( error, path ); + }.bind( this ); + + var onLast = function( response ) { + this.lastPageReached( response, path ); + }.bind( this ); + + request( path, this.options.responseType, onLoad, onError, onLast ); + this.dispatchEvent( 'request', null, [ path ] ); +}; + +proto.onPageLoad = function( response, path ) { + // done loading if not appending + if ( !this.options.append ) { + this.isLoading = false; + } + this.pageIndex++; + this.loadCount++; + this.dispatchEvent( 'load', null, [ response, path ] ); + this.appendNextPage( response, path ); + return response; +}; + +proto.appendNextPage = function( response, path ) { + var optAppend = this.options.append; + // do not append json + var isDocument = this.options.responseType == 'document'; + if ( !isDocument || !optAppend ) { + return; + } + + var items = response.querySelectorAll( optAppend ); + var fragment = getItemsFragment( items ); + var appendReady = function () { + this.appendItems( items, fragment ); + this.isLoading = false; + this.dispatchEvent( 'append', null, [ response, path, items ] ); + }.bind( this ); + + // TODO add hook for option to trigger appendReady + if ( this.options.outlayer ) { + this.appendOutlayerItems( fragment, appendReady ); + } else { + appendReady(); + } +}; + +proto.appendItems = function( items, fragment ) { + if ( !items || !items.length ) { + return; + } + // get fragment if not provided + fragment = fragment || getItemsFragment( items ); + refreshScripts( fragment ); + this.element.appendChild( fragment ); +}; + +function getItemsFragment( items ) { + // add items to fragment + var fragment = document.createDocumentFragment(); + for ( var i=0; items && i < items.length; i++ ) { + fragment.appendChild( items[i] ); + } + return fragment; +} + +// replace