From a8f26192676da86032fc80ffe440875d2c7b2023 Mon Sep 17 00:00:00 2001 From: "Jordi BERTRAN DE BALANDA (swhintern)" Date: Wed, 15 Jun 2016 12:16:35 +0200 Subject: [PATCH] swhsearchbox: provide drag & drop user interface --- .../themes/layerswp-swh/assets/js/search.js | 279 +++++++++++++++++++-- .../wp-content/themes/layerswp-swh/functions.php | 48 +++- 2 files changed, 304 insertions(+), 23 deletions(-) diff --git a/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js b/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js index 91acb2d..2c087de 100644 --- a/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js +++ b/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js @@ -4,7 +4,7 @@ var CHECKSUM_RE = /^([0-9a-f]{40}|[0-9a-f]{64})$/i; function swh_search_msg(msg, cls) { var html; html = '' + msg + ''; - jQuery('#swh-search-result').html(html); + jQuery('#swh-search-message').html(html); } function swh_search_show_results(j, status) { @@ -24,21 +24,270 @@ function swh_fail_result() { swh_search_msg('The Software Heritage API server is unavailable. Please try again later.', 'error'); } +/** + * Client-side hashing utils + */ +var nameList = []; /** Avoid adding the same file twice **/ + +function handleFiles(myfiles, fileLister, searchForm) { + for (var i = 0; i < myfiles.length; i++) { + var file = myfiles.item(i); + if (nameList.indexOf(file.name) == -1) { + nameList.push(file.name); + var fr = new FileReader(); + fileLister.append(make_row(file.name)); + bind_reader(fr, file.name, searchForm); + fr.readAsArrayBuffer(file); + } + } +}; + +function bind_reader(filereader, filename, searchForm) { + filereader.onloadend = function(evt) { + if (evt.target.readyState == FileReader.DONE){ + return fileReadDone(evt.target.result, filename, searchForm); + } + }; +} + +function make_row(name) { + return '
'+name+'
'; +} + +function fileReadDone(buffer, fname, searchForm) { + var wordArray = CryptoJS.lib.WordArray.create(buffer); + var sha1 = CryptoJS.SHA1(wordArray); + /** + var git_hd = "blob " + wordArray.length + "\0"; + var git_array = CryptoJS.enc.utf8.parse(git_hd).concat(wordArray); + var sha256 = CryptoJS.SHA256(wordArray); + var sha1_git = CryptoJS.SHA1(git_array); + **/ + searchForm.append($("", {type: "hidden", + name: fname, + value: sha1} + )); +} + +/** + * JQuery setup + */ + jQuery(document).ready(function($){ - $('#swh-search-submit').click(function(event){ - event.preventDefault(); - var q = $('#swh-search-query').val(); - if (!q || !q.match(CHECKSUM_RE)) - swh_search_msg('Please enter a file checksum in the search box.', 'error'); - else - $.ajax({ - url: SWH_API_URL + '/search/' + q + '/', - dataType: 'json', - beforeSend: function (xhr) { - swh_search_msg('Searching for a file with hash ' + q + '...', ''); - }, - success: swh_search_show_results, - error: swh_fail_result, + + $.fn.extend({ + /** + * Call on any HTMLElement to make that element the recipient of files + * drag & dropped into it. + * Files then have their sha1 checksum calculated + * and searched in SWH. + * Args: + * resultDiv: the table where the result should be displayed + * errorDiv: the element where the error message should be displayed + */ + filedrop: function(fileLister, searchForm) { + + return this.each(function() { + + var dragwin = $(this); + var fileshovering = false; + + dragwin.on('dragover', function(event) { + event.stopPropagation(); + event.preventDefault(); + }); + + dragwin.on('dragenter', function(event) { + event.stopPropagation(); + event.preventDefault(); + if (!fileshovering) { + dragwin.css("border-style", "solid"); + dragwin.css("box-shadow", "inset 0 3px 4px"); + fileshovering = true; + } + }); + + dragwin.on('dragover', function(event) { + event.stopPropagation(); + event.preventDefault(); + if (!fileshovering) { + dragwin.css("border-style", "solid"); + dragwin.css("box-shadow", "inset 0 3px 4px"); + fileshovering = true; + } + }); + + dragwin.on('dragleave', function(event) { + event.stopPropagation(); + event.preventDefault(); + if (fileshovering) { + dragwin.css("border-style", "dashed"); + dragwin.css("box-shadow", "none"); + fileshovering = false; + } + }); + + dragwin.on('drop', function(event) { + event.stopPropagation(); + event.preventDefault(); + if (fileshovering) { + dragwin.css("border-style", "dashed"); + dragwin.css("box-shadow", "none"); + fileshovering = false; + } + var myfiles = event.originalEvent.dataTransfer.files; + if (myfiles.length >= 1) { + handleFiles(myfiles, fileLister, searchForm); + } + }); }); + }, + /** + * Call on a jQuery-selected input to make it sensitive to + * the reception of new files, and have it process received + * files. + * Args: + * fileLister: the element keeping track of the files + * searchForm: the form whose submission will POST the file + * information + */ + filedialog: function(fileLister, searchForm) { + return this.each(function() { + var elem = $(this); + elem.on('change', function(){ + handleFiles(this.files, fileLister, searchForm); + }); + }); + }, + /** + * Call on a jQuery-selected element to delegate its click + * event to the given input instead. + * Args: + * input: the element to be clicked when the caller is clicked. + */ + inputclick: function(input) { + return this.each(function() { + $(this).click(function(event) { + event.preventDefault(); + input.click(); + }); + }); + }, + /** + * Call on a search form to delegate its click event to search + * the files via an AJAX call + * Args: + * resultElem: the element containing the result table and + * the result text after completion of the AJAX call + */ + ajaxResults: function(resultElem) { + return this.each(function() { + $(this).submit(function(e) { + e.preventDefault(); + var q = $('#swh-search-query').val(); + if (q && !q.match(CHECKSUM_RE)) + swh_search_msg('Please enter a file checksum in the search box.', 'error'); + else + $.ajax({ + method: 'POST', + url: 'http://localhost:6543/api/1/search/', + dataType: 'json', + data: $(this).serialize(), + beforeSend: function (xhr) { + swh_search_msg('Searching for your files...', ''); + }, + success: function(data) { + ajaxSearchSuccess(data, resultElem); + swh_search_msg('Search complete!', ''); + }, + error: swh_fail_result, + }); + }); + }); + }, + /** + * Update an element to reflect the search result stats + * Args: + * statsObj: the stats object {nbfile:, pct:} + */ + updateStats: function(statsObj) { + var pct = statsObj.pct; + var nbf = statsObj.nbfiles; + var hue=(pct*120).toString(10); + $(this).css('background-color', ["hsl(",hue,",45%,48%)"].join("")); + $(this).text('Found ' + pct.toFixed(2) + '% of ' + nbf + ' files (click to expand).'); + }, + /** + * Update an element to contain a new result in the result table + * Args: + * resultObj: the result object {filename:, sha1:, found:} + */ + addResult: function(resultObj) { + var found = resultObj.found?'Yes':'No'; + var classfound = resultObj.found?'found':'not-found'; + var checksum = resultObj.sha1; + var fname = !resultObj.filename?'From input': resultObj.filename; + $(this).append($('') + .append($('').append(fname)) + .append($('').append(checksum)) + .append($('').append($('') + .addClass(classfound) + .text(found)))); + }, + /** + * Call on an element to make it collapsible on a hijacked click from the + * target button + * Args: + * collapser: the element that will toggle the collapse + */ + collapses: function(collapsed) { + return this.each(function() { + collapsed.css('display', 'none'); + $(this).click(function (event) { + event.preventDefault(); + collapsed.slideToggle('fast'); + }); + }); + } }); + + function ajaxSearchSuccess(data, resultElem) { + // Prepare stats and result + var statsDiv = $('') + .attr('id', 'collapse-button') + .css('border-style', 'none'); + statsDiv.updateStats(data.search_stats); + var resultTab = $('
') + .attr('id', 'collapsible-search-results') + .append($('') + .append($('').text('Origin')) + .append($('').text('SHA1 checksum')) + .append($('').text('Found in SWH'))) + for (let resE of data.search_res) { + if (resE.filename == 'q') + resE.filename = 'From text input'; + resultTab.addResult(resE); + } + // Change the DOM + resultElem.empty(); + resultElem.append(statsDiv); + resultElem.append(resultTab); + // Set the click listener + $("#collapse-button").collapses($("#collapsible-search-results")); + } + + function urlizeSHA1(sha1){ + return $('') + .attr('href', '/browse/' + sha1) + .text(sha1); + } + + // Delegate dropbox click + $("#swh-search-dropbox").inputclick($("#files-input")); + // Make the dropbox available for drag & drop + $("#files-input").filedialog($("#hashed-file-list"), $("#swh-search")); + // Make the submission button receptive to files + $("#swh-search-dropbox").filedrop($("#hashed-files-list"), $("#swh-search")); + // Hijack the form submission for ajax + $("#swh-search").ajaxResults($("#swh-search-results")); }); diff --git a/htdocs/wp-content/themes/layerswp-swh/functions.php b/htdocs/wp-content/themes/layerswp-swh/functions.php index 8b8fe1c..336c90b 100644 --- a/htdocs/wp-content/themes/layerswp-swh/functions.php +++ b/htdocs/wp-content/themes/layerswp-swh/functions.php @@ -84,23 +84,55 @@ add_shortcode('swhcounter', 'swh_shortcode_counter'); // template for generating SWH search box // Usage: [swhsearchbox] function swh_shortcode_search_box( $atts, $content = null ) { - wp_enqueue_script( // load client-side search code + wp_enqueue_script( // load client-side search code 'layers-swh-search', get_stylesheet_directory_uri() . '/assets/js/search.js', array() - ); - ob_start(); - ?> -