From 7d16001a18077dd33f4d93570761f79fe09a24f4 Mon Sep 17 00:00:00 2001 From: Luc Jaulmes Date: Fri, 8 Feb 2019 17:01:35 +0100 Subject: [PATCH] Add a searchable box TODO add a delay for smoothness (and min chars: 2?) --- core-cfp.css | 43 +++++---- core-cfp.js | 262 ++++++++++++++++++++++++++++++++++----------------- index.html | 11 ++- 3 files changed, 211 insertions(+), 105 deletions(-) diff --git a/core-cfp.css b/core-cfp.css index 0e034dd..afc1fae 100644 --- a/core-cfp.css +++ b/core-cfp.css @@ -73,9 +73,9 @@ body > *:not(#timeline_container) { } } -#filters { - margin: 5em auto 0; - padding: 16px 2px; +#filters, #search { + margin: 1em auto 0; + padding: 0px 2px 10px; border: thin solid; border-radius: 10px; display: flex; @@ -84,19 +84,7 @@ body > *:not(#timeline_container) { align-items: baseline; } -#filterbox { - position: relative; -} - -#filterbox h5 { - position: absolute; - top: -.5em; - left: 15px; - background: #fff; - padding: 0 3px; - margin: 0; -} - +#search p, #filters p { margin: 0; padding: 0 .5em; @@ -104,12 +92,29 @@ body > *:not(#timeline_container) { text-align: center; } +#search p { + display: flex; + width: 100%; +} + +#search p input { + flex-grow: 1; + margin: 0 10px 0 0; +} + +#search dt { + float: none; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 0; +} + #filters p > * { width: 100%; } #filters select[name=conf] { - height: 48ex; + max-height: 45ex; } #confs_wrapper { @@ -246,6 +251,10 @@ span.acronyms { #months .timeblocks span { background-color: #eee; border-left: black thin solid; +} + +#years .timeblocks span:last-child, +#months .timeblocks span:last-child { border-right: black thin solid; } diff --git a/core-cfp.js b/core-cfp.js index 0cf3789..9da6dde 100644 --- a/core-cfp.js +++ b/core-cfp.js @@ -4,16 +4,18 @@ const abstIdx = 4, subIdx = 5, notifIdx = 6, camIdx = 7, startIdx = 8, endIdx = const yearIdx = 4, yearOffset = 14, origOffset = 6; const today = new Date(), year = today.getFullYear(); +const month_name = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const timeline_zero = Date.UTC(today.getFullYear(), today.getMonth() - 6, 1); // some global variables -var timeline_max = Date.UTC(today.getFullYear(), today.getMonth() + 18, 1); +var timeline_max = Date.UTC(today.getFullYear(), today.getMonth() + 18, 0); // % per month: 50px / duration of 1 month var timeline_scale = 100 / (timeline_max - timeline_zero); var datatable, timeline = document.getElementById('timeline'), n_years = 1; var timeline_dom_cache = {}; -var filters = document.getElementById('filters'); +var form = document.querySelector('form'); // the value we push into the hash var sethash = ''; @@ -27,7 +29,7 @@ function ranksort(a, b) function parseFragment() { var hash = window.location.hash.substr(1).split('&'); - var goto = null; + var goto = undefined; var result = hash.reduce(function (result, item) { @@ -40,28 +42,23 @@ function parseFragment() result[parts[0]].push(decodeURIComponent(parts[1])); } - else if (item && $('#' + item).length) - goto = item; + else if (item && document.getElementById(item)) + goto = window.pageYOffset + document.getElementById(item).getBoundingClientRect().top; return result; }, {}); - if (result.length && goto) - $('html, body').scrollTop($('#' + goto).offset().top); + if (result.length && goto !== undefined) + window.scroll(window.pageXOffset, goto); return result; } function updateFragment() { - var params = $(filters).find('select').serializeArray().map(function (item) - { - return item['name'] + '=' + item['value'] - }); - params = params.sort().filter(function (it, pos, arr) - { - return !pos || it != arr[pos - 1]; - }); + var params = Array.from(form.querySelectorAll('select')).reduce( + (params, sel) => params.concat(Array.from(sel.selectedOptions).map(opt => sel.name + '=' + opt.value)) + , []).sort().filter((it, pos, arr) => pos === 0 || it !== arr[pos - 1]); /* get last part of &-separated fragment that contains no '=' */ var goto = window.location.hash.substr(1).split('&').reduce(function (prev, item) @@ -79,45 +76,48 @@ function updateFragment() function makeTimelineLegend() { - var box = $('#timeline_header'); + var box = document.getElementById('timeline_header'); + while (box.hasChildNodes()) + box.firstChild.remove(); + var startDate = new Date(timeline_zero), endDate = new Date(timeline_max); - box.empty() + var months = document.createElement('p'); + months.id = 'months'; + months.appendChild(document.createElement('span')).className += 'acronyms'; + months.appendChild(document.createElement('span')).className += 'timeblocks'; - var pm = $('

 

').prependTo(box).find('.timeblocks'), - last = timeline_zero, - month_name = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - m; - - for (m = today.getMonth() - 6; last < endDate; m++) + for (var m = startDate.getMonth(); m <= endDate.getMonth() + 12 * (endDate.getFullYear() - startDate.getFullYear()); m++) { - var date = Date.UTC(today.getFullYear(), m, 1), - next = Date.UTC(today.getFullYear(), m + 1, 1), - month = new Date(last).getMonth(); - - $('').append(month_name[month]) - .addClass(m == today.getMonth() - 6 || month == 0 ? 'first' : '') - .css('width', (next - date) * timeline_scale + '%') - .css('left', (date - timeline_zero) * timeline_scale + '%').appendTo(pm); - - last = next; + var from = Date.UTC(startDate.getFullYear(), m, 1); + var until = Date.UTC(startDate.getFullYear(), m + 1, 0); + + var month = months.lastChild.appendChild(document.createElement('span')); + month.textContent = month_name[m % 12]; + month.style.width = (until - from) * timeline_scale + '%' + month.style.left = (from - timeline_zero) * timeline_scale + '%' + if (m == startDate.getMonth() || m % 12 == 0) month.className += 'first'; } - var py = $('

 

').prependTo(box).find('.timeblocks'); + var years = document.createElement('p'); + years.id = 'years'; + years.appendChild(document.createElement('span')).className += 'acronyms'; + years.appendChild(document.createElement('span')).className += 'timeblocks'; for (var y = startDate.getFullYear(); y <= endDate.getFullYear(); y++) { var from = Math.max(timeline_zero, Date.UTC(y, 0, 1)); - var to = Math.min(last, Date.UTC(y + 1, 0, 1)); - $('').append(y).css('width', 'calc(' + (to - from) * timeline_scale + '% - 1px)').appendTo(py) - .css('left', (from - timeline_zero) * timeline_scale + '%').appendTo(py); + var until = Math.min(timeline_max, Date.UTC(y + 1, 0, 0)); + + var year = years.lastChild.appendChild(document.createElement('span')); + year.textContent = y; + year.style.width = 'calc(' + (until - from) * timeline_scale + '% - 1px)'; + year.style.left = (from - timeline_zero) * timeline_scale + '%'; } - $('#timeline').on('scroll', function (obj) - { - $('#timeline_header').scrollLeft(obj.currentTarget.scrollLeft); - }); + box.appendChild(years); + box.appendChild(months); } function parse_date(str) @@ -251,59 +251,149 @@ async function addToTimeline(n, data) timeline.appendChild(p); } +function searchWords() +{ + var search = form.querySelector('input[name="words"]').value; + return search.split(/[ ;:,.]/).filter(val => val); +} + +function setColumnSearch(select, column) +{ + var val = Array.from(select.selectedOptions).map(opt => $.fn.dataTable.util.escapeRegex(opt.value)); + var regex = val.length ? ('^(' + val.join('|') + ')$') : ''; + + if (form.querySelector('select[name="scope"]').value == select.getAttribute('column_id')) + { + var search = searchWords(); + if (val.length && search.length) + regex += '|'; + regex += search.map(val => $.fn.dataTable.util.escapeRegex(val)).join('|'); + } + + column.search(regex, true, false); +} + +function setGlobalSearch(col) +{ + if (col < 0) + datatable.search(searchWords().map(val => $.fn.dataTable.util.escapeRegex(val)).join('|'), true, false) + else + datatable.search(''); +} + +// this is the select +function updateFilter() +{ + var column = datatable.column(this.getAttribute('column_id')); + setColumnSearch(this, column); + + new Promise(done => + { + column.draw(); + done(); + }).then(updateFragment); +} + +function updateSearch() +{ + var col = parseInt(form.querySelector('select[name="scope"]').value); + + setGlobalSearch(col); + if (col >= 0) + setColumnSearch(form.querySelector('select[column_id="' + col + '"]'), datatable.column(col)); + + new Promise(done => + { + datatable.draw(); + done(); + }).then(updateFragment); +} + +// this is select.name="scope" +function updateSearchScope() +{ + var col = parseInt(this.value); + + setGlobalSearch(col); + Array.from(this.options).map(opt => parseInt(opt.value)).forEach(val => + { + if (val >= 0) + setColumnSearch(form.querySelector('select[column_id="' + val + '"]'), datatable.column(col)); + }); + + new Promise(done => + { + datatable.draw(); + done(); + }).then(updateFragment); +} + async function filterUpdated(search) { while (timeline.hasChildNodes()) timeline.firstChild.remove(); - $('#loading').show(); + var loading = document.getElementById('loading'); + loading.style.display = 'block'; + var filteredData = datatable.rows({ filter: 'applied' }).data(); $.each(filteredData, addToTimeline); - $('#loading').hide(); + + loading.style.display = 'none'; } -function makeFilter(column, name, initFilters, sortfunction) +function makeFilter(colIdx, name, sortfunction) { + var column = datatable.column(colIdx); var values = column.data().unique().sort(sortfunction); - var select = $('').attr('name', name).attr('size', values.length); - var p = $('

').append(column.header().innerHTML).append(select).append( - $('').click(function () - { - select.val([]).change() - }) - ); + var opt = form.querySelector('select[name="scope"]').appendChild(document.createElement('option')); + opt.value = colIdx; + + var p = document.createElement('p'); + opt.textContent = p.textContent = column.header().textContent; + + var select = p.appendChild(document.createElement('select')); + select.multiple = true; + select.name = name; + select.size = values.length; + select.setAttribute('column_id', colIdx); + + var clear = p.appendChild(document.createElement('button')); + clear.textContent = 'clear'; - var selected = initFilters[name] ? initFilters[name] : []; values.each(function (t) { - select.append(''); + var option = select.appendChild(document.createElement('option')); + option.textContent = t; + option.value = t; }); - select.on('change', function () + select.onchange = updateFilter + clear.onclick = () => { - var val = $.map($(this).val(), $.fn.dataTable.util.escapeRegex), - regex = ''; - if (val.length) - regex = '^(' + val.join('|') + ')$'; - column.search(regex, true, false); - new Promise(done => {column.draw(); done();}).then(() => updateFragment()); - }); + select.selectedIndex = -1; + column.search(''); + updateFilter.call(select); + }; - if (selected.length) - select.change(); - - return p[0]; + return p; } -function updateFilters() +function filterFromFragment() { - var filters = parseFragment(); - $('#filters select').each(function (i, obj) + var selects = Array.from(form.querySelectorAll('select')); + var selectedValues = parseFragment(); + + selects.forEach(sel => { - var sel = $(obj); - sel.val(filters[sel.attr('name')] || []).change(); - }) + sel.selectedIndex = -1; + var values = selectedValues[sel.name] || (sel.name == 'scope' ? ['0'] : []); + if (!values.length) return; + Array.from(sel.options).forEach(opt => { opt.selected = values.indexOf(opt.value) >= 0 }); + }); + + selects.filter(sel => sel.name !== 'scope').forEach(sel => sel.onchange()); } function renderAcronym(data, type, row) @@ -336,7 +426,7 @@ function renderAcronym(data, type, row) function markExtrapolated(td, data, rowdata, row, col) { if (data && rowdata[col + origOffset] === false) - $(td).addClass('extrapolated'); + td.className += 'extrapolated'; } function notNull(val, idx) @@ -401,17 +491,20 @@ function populatePage(json) ] }); - $('#head').append(' The last scraping took place on ' + json['date'] + '.') + document.getElementById('head').textContent += ' The last scraping took place on ' + json['date'] + '.'; - var initFilters = parseFragment(); + var filters = document.getElementById('filters'); + filters.appendChild(makeFilter(confIdx, "conf")); + filters.appendChild(makeFilter(rankIdx, "core", ranksort)); + filters.appendChild(makeFilter(fieldIdx, "field")); - filters.appendChild(makeFilter(datatable.column(confIdx), "conf", initFilters)); - filters.appendChild(makeFilter(datatable.column(rankIdx), "core", initFilters, ranksort)); - filters.appendChild(makeFilter(datatable.column(fieldIdx), "field", initFilters)); + form.querySelector('select[name="scope"]').onchange = updateSearchScope + form.querySelector('input[name="words"]').onkeypress = updateSearch - updateFragment(); + // Initial fragment + filterFromFragment(); - $(window).on('hashchange', updateFilters); + window.addEventListener('hashchange', filterFromFragment); datatable.draw().on('search.dt', filterUpdated); var maxdate = timeline_zero; @@ -421,22 +514,17 @@ function populatePage(json) // lexical sort works thanks to format, just parse once maxdate = Math.max(maxdate, parse_date(colmax)); } - var endDate = new Date(maxdate); // get last day of month - // go to first day to avoid side effects of incrementing month - endDate.setDate(1); - endDate.setMonth(endDate.getMonth() + 1); - endDate.setDate(0); - timeline_max = endDate.getTime(); + var endDate = new Date(maxdate); + timeline_max = Date.UTC(endDate.getFullYear(), endDate.getMonth() + 1, 0); timeline_scale = 100 / (timeline_max - timeline_zero); - //$('#timeline').width('calc(8.5em + ' + (timeline_max - timeline_zero) * timeline_scale + 'px)'); makeTimelineLegend(); // add data to Timeline, but only filtered filterUpdated(); - $('#loading').hide() + document.getElementById('loading').style.display = 'none'; } $(document).ready(function () diff --git a/index.html b/index.html index 8740f7f..5bc6772 100644 --- a/index.html +++ b/index.html @@ -55,7 +55,16 @@

Computer Science conferences timeline

Dates unavailable, extrapolated from last years'
-
+
+ +
Filters
+