jenkins-bot has submitted this change and it was merged. Change subject: Refactor gui.js ......................................................................
Refactor gui.js Refactor gui.js to be oo and split up into several classes. Bug: T116464 Change-Id: I771c0113503c97c14a4fc6b210ad7e2226570261 --- D gui/gui.js M gui/index.html D gui/js/wikibase/queryService/Api.js D gui/js/wikibase/queryService/ui/App.js D gui/js/wikibase/queryService/ui/Editor.js M gui/style.css A gui/wikibase/init.js A gui/wikibase/queryService/RdfNamespaces.js A gui/wikibase/queryService/api/QuerySamples.js A gui/wikibase/queryService/api/Sparql.js A gui/wikibase/queryService/ui/App.js A gui/wikibase/queryService/ui/Editor.js 12 files changed, 1,053 insertions(+), 889 deletions(-) Approvals: Smalyshev: Looks good to me, approved jenkins-bot: Verified diff --git a/gui/gui.js b/gui/gui.js deleted file mode 100644 index e0c3819..0000000 --- a/gui/gui.js +++ /dev/null @@ -1,756 +0,0 @@ -window.mediaWiki = window.mediaWiki || {}; -window.EDITOR = {}; - -( function ( $, mw ) { - var SERVICE = '/bigdata/namespace/wdq/sparql', - SHORTURL = 'http://tinyurl.com/create.php?url=', - EXPLORE_URL = 'http://www.wikidata.org/entity/Q', - NAMESPACE_SHORTCUTS = { - 'Wikidata': { - 'wikibase': 'http://wikiba.se/ontology#', - 'wd': 'http://www.wikidata.org/entity/', - 'wdt': 'http://www.wikidata.org/prop/direct/', - 'wds': 'http://www.wikidata.org/entity/statement/', - 'p': 'http://www.wikidata.org/prop/', - 'wdref': 'http://www.wikidata.org/reference/', - 'wdv': 'http://www.wikidata.org/value/', - 'ps': 'http://www.wikidata.org/prop/statement/', - 'psv': 'http://www.wikidata.org/prop/statement/value/', - 'pq': 'http://www.wikidata.org/prop/qualifier/', - 'pqv': 'http://www.wikidata.org/prop/qualifier/value/', - 'pr': 'http://www.wikidata.org/prop/reference/', - 'prv': 'http://www.wikidata.org/prop/reference/value/', - 'wdno': 'http://www.wikidata.org/prop/novalue/', - 'wdata': 'http://www.wikidata.org/wiki/Special:EntityData/' - }, - 'W3C': { - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', - 'owl': 'http://www.w3.org/2002/07/owl#', - 'skos': 'http://www.w3.org/2004/02/skos/core#', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - 'prov': 'http://www.w3.org/ns/prov#' - }, - 'Social/Other': { - 'schema': 'http://schema.org/' - }, - 'Blazegraph': { - 'bd': 'http://www.bigdata.com/rdf#', - 'bds': 'http://www.bigdata.com/rdf/search#', - 'gas': 'http://www.bigdata.com/rdf/gas#', - 'hint': 'http://www.bigdata.com/queryHints#' - } - }, - STANDARD_PREFIXES = [ - 'PREFIX wd: <http://www.wikidata.org/entity/>', - 'PREFIX wdt: <http://www.wikidata.org/prop/direct/>', - 'PREFIX wikibase: <http://wikiba.se/ontology#>', - 'PREFIX p: <http://www.wikidata.org/prop/>', - 'PREFIX v: <http://www.wikidata.org/prop/statement/>', - 'PREFIX q: <http://www.wikidata.org/prop/qualifier/>', - 'PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>' - ].join( '\n' ), - QUERY_START = 0, - CODEMIRROR_DEFAULTS = { - lineNumbers: true, - matchBrackets: true, - mode: 'sparql', - extraKeys: { 'Ctrl-Space': 'autocomplete' }, - viewportMargin: Infinity - }, - ERROR_LINE_MARKER = null, - ERROR_CHARACTER_MARKER = null, - LAST_RESULT = null, - DOWNLOAD_FORMATS = { - 'CSV': { - handler: getCsvData, - mimetype: 'text/csv;charset=utf-8' - }, - 'JSON': { - handler: getJsonData, - mimetype: 'application/json;charset=utf-8' - }, - 'TSV': { - handler: getSparqlTsvData, - mimetype: 'text/tab-separated-values;charset=utf-8' - }, - 'Simple TSV': { - handler: getSimpleTsvData, - mimetype: 'text/tab-separated-values;charset=utf-8', - ext: 'tsv' - }, - 'Full JSON': { - handler: getAllJsonData, - mimetype: 'application/json;charset=utf-8', - ext: 'json' - } - }; - - /** - * Submit SPARQL query. - * - * @param {Event} e - */ - function submitQuery( e ) { - e.preventDefault(); - EDITOR.save(); - - LAST_RESULT = null; - - var query = $( '#query-form' ).serialize(), - hash = encodeURIComponent( EDITOR.getValue() ), - url = SERVICE + '?' + query, - settings = { - headers: { - 'Accept': 'application/sparql-results+json' - }, - success: showQueryResults, - error: queryResultsError - }; - $( '#query-result' ).empty( '' ); - $( '#query-result' ).hide(); - $( '#total' ).hide(); - $( '#query-error' ).show(); - $( '#query-error' ).text( 'Running query...' ); - if ( window.location.hash !== hash ) { - window.location.hash = hash; - } - QUERY_START = Date.now(); - $.ajax( url, settings ); - } - - /** - * Handle SPARQL error. - * - * @param {Object} jqXHR - * @param {string} textStatus - * @param {string} errorThrown - */ - function queryResultsError( jqXHR, textStatus, errorThrown ) { - var response, - message = 'ERROR: '; - - if ( jqXHR.status === 0 ) { - message += 'Could not contact server'; - } else { - response = $( '<div>' ).append( jqXHR.responseText ); - message += response.text(); - highlightError( jqXHR.responseText ); - if ( jqXHR.responseText.match( /Query deadline is expired/ ) ) { - message = 'QUERY TIMEOUT\n' + message; - } - } - $( '#query-error' ).html( $( '<pre>' ).text( message ) ).show(); - } - - /** - * Show results of the query. - * - * @param {Object} data - */ - function showQueryResults( data ) { - var results, thead, i, tr, td, linkText, j, binding, - table = $( '<table>' ) - .attr( 'class', 'table' ) - .appendTo( $( '#query-result' ) ); - $( '#query-error' ).hide(); - $( '#query-result' ).show(); - - if ( typeof data.boolean !== 'undefined' ) { - // ASK query - table.append( '<tr><td>' + data.boolean + '</td></tr>' ).addClass( 'boolean' ); - return; - } - - LAST_RESULT = data; - results = data.results.bindings.length; - $( '#total-results' ).text( results ); - $( '#query-time' ).text( Date.now() - QUERY_START ); - $( '#total' ).show(); - $( '#shorturl' ).attr( 'href', SHORTURL + encodeURIComponent( window.location ) ); - - thead = $( '<thead>' ).appendTo( table ); - tr = $( '<tr>' ); - for ( i = 0; i < data.head.vars.length; i++ ) { - tr.append( '<th>' + data.head.vars[i] + '</th>' ); - } - thead.append( tr ); - table.append( thead ); - - for ( i = 0; i < results; i++ ) { - tr = $( '<tr>' ) ; - for ( j = 0; j < data.head.vars.length; j++ ) { - td = $( '<td>' ) ; - if ( data.head.vars[j] in data.results.bindings[i] ) { - binding = data.results.bindings[i][data.head.vars[j]]; - var text = binding.value; - if ( binding.type === 'uri' ) { - text = abbreviate( text ); - } - linkText = $( '<pre>' ).text( text.trim() ); - if ( binding.type === 'typed-literal' ) { - td.attr( { - 'class': 'literal', - 'data-datatype': binding.datatype - } ).append( linkText ); - } else { - td.attr( 'class', binding.type ); - if ( binding.type === 'uri' ) { - td.append( $( '<a>' ) - .attr( 'href', binding.value ) - .append( linkText ) - ); - if ( binding.value.match( EXPLORE_URL ) ) { - td.append( $( '<a>' ) - .attr( 'href', '#' ) - .bind( 'click', exploreUrl.bind( undefined, binding.value ) ) - .text( '*' ) - ); - } - } else { - td.append( linkText ); - } - - if ( binding['xml:lang'] ) { - td.attr( { - 'data-lang': binding['xml:lang'], - title: binding.value + '@' + binding['xml:lang'] - } ); - } - } - } else { - // no binding - td.attr( 'class', 'unbound' ); - } - tr.append( td ); - } - table.append( tr ); - } - } - - /** - * Produce abbreviation of the URI. - * - * @param {string} uri - * @returns {string} - */ - function abbreviate( uri ) { - var nsGroup, ns; - - for ( nsGroup in NAMESPACE_SHORTCUTS ) { - for ( ns in NAMESPACE_SHORTCUTS[nsGroup] ) { - if ( uri.indexOf( NAMESPACE_SHORTCUTS[nsGroup][ns] ) === 0 ) { - return uri.replace( NAMESPACE_SHORTCUTS[nsGroup][ns], ns + ':' ); - } - } - } - return '<' + uri + '>'; - } - - /** - * Add standard prefixes to editor window. - */ - function addPrefixes() { - var current = EDITOR.getValue(); - EDITOR.setValue( STANDARD_PREFIXES + '\n\n' + current ); - } - - /** - * Populate namespace shortcut selector. - */ - function populateNamespaceShortcuts() { - var category, select, ns, - container = $( '.namespace-shortcuts' ); - - container.click( function ( e ) { - e.stopPropagation(); - } ); - - // add namespaces to dropdowns - for ( category in NAMESPACE_SHORTCUTS ) { - select = $( '<select>' ) - .attr( 'class', 'form-control' ) - .append( $( '<option>' ).text( category ) ) - .appendTo( container ); - for ( ns in NAMESPACE_SHORTCUTS[category] ) { - select.append( $( '<option>' ).text( ns ).attr( { - value: NAMESPACE_SHORTCUTS[category][ns] - } ) ); - } - } - } - - /** - * Add selected namespace's prefix to editor. - */ - function selectNamespace() { - var ns, - uri = this.value, - current = EDITOR.getValue(); - - if ( current.indexOf( '<' + uri + '>' ) === -1 ) { - ns = $( this ).find( ':selected' ).text(); - EDITOR.setValue( 'prefix ' + ns + ': <' + uri + '>\n' + current ); - } - - // reselect group label - this.selectedIndex = 0; - } - - /** - * Show/hide help text. - * - * @param {Event} e - */ - function showHideHelp( e ) { - var $seeAlso = $( '#seealso' ); - - e.preventDefault(); - $seeAlso.toggle(); - if ( $seeAlso.is( ':visible' ) ) { - $( '#showhide' ).text( 'hide' ); - } else { - $( '#showhide' ).text( 'show' ); - } - } - - /** - * Initialize query editor window. - */ - function initQuery() { - if ( window.location.hash !== '' ) { - EDITOR.setValue( decodeURIComponent( window.location.hash.substr( 1 ) ) ); - EDITOR.refresh(); - } - } - - /** - * Setup editor window. - */ - function setupEditor() { - EDITOR = CodeMirror.fromTextArea( $( '#query' )[0], CODEMIRROR_DEFAULTS ); - EDITOR.on( 'change', function () { - if ( ERROR_LINE_MARKER ) { - ERROR_LINE_MARKER.clear(); - ERROR_CHARACTER_MARKER.clear(); - } - } ); - EDITOR.addKeyMap( { 'Ctrl-Enter': submitQuery } ); - EDITOR.focus(); - - new WikibaseRDFTooltip(EDITOR); - } - - /** - * Highlight SPARQL error in editor window. - * - * @param {string} description - */ - function highlightError( description ) { - var line, character, - match = description.match( /line (\d+), column (\d+)/ ); - if ( match ) { - // highlight character at error position - line = match[1] - 1; - character = match[2] - 1; - ERROR_LINE_MARKER = EDITOR.doc.markText( - { line: line, ch: 0 }, - { line: line }, - { className: 'error-line' } - ); - ERROR_CHARACTER_MARKER = EDITOR.doc.markText( - { line: line, ch: character }, - { line: line, ch: character + 1 }, - { className: 'error-character' } - ); - } - } - - /** - * Show explorer window for given URL. - * - * @param {string} url - */ - function exploreUrl( url ) { - var id, - match = url.match( EXPLORE_URL + '(.+)' ); - if ( !match ) { - return; - } - if ( $( '#hide-explorer' ).is( ':visible' ) ) { - $( '#explore' ).empty( '' ); - } else { - $( '#hide-explorer' ).show(); - $( '#show-explorer' ).hide(); - } - id = match[1]; - mw.config = { get: function () { - return id; - } }; - $( 'html, body' ).animate( { scrollTop: $( '#explore' ).offset().top }, 500 ); - EXPLORER( $, mw, $( '#explore' ) ); - } - - /** - * Hide explorer window. - * - * @param {Event} e - */ - function hideExplorer( e ) { - e.preventDefault(); - $( '#explore' ).empty( '' ); - $( '#hide-explorer' ).hide(); - $( '#show-explorer' ).show(); - } - - /** - * Setup query examples. - */ - function setupExamples() { - var exampleQueries = document.getElementById( 'exampleQueries' ); - - $.ajax( { - url: 'https://www.mediawiki.org/w/api.php?action=query&prop=revisions&titles=Wikibase/' - + 'Indexing/SPARQL_Query_Examples&rvprop=content', - data: { - format: 'json' - }, - dataType: 'jsonp' - } ).done( function ( data ) { - var wikitext = data.query.pages[Object.keys( data.query.pages )].revisions[0]['*']; - var paragraphs = wikitext.split( '==' ); - - $.each( paragraphs, function ( key, paragraph ) { - if ( paragraph.match( /SPARQL\|.*query\=/ ) ) { - var query = paragraph.substring( - paragraph.indexOf( '|query=' ) + 7, - paragraph.lastIndexOf( '}}' ) - ).trim(); - var title = paragraphs[key - 1] || ''; - title = title.replace( '=', '' ).trim(); - - if ( paragraph.match( 'extraprefix=' ) ) { - var prefix = paragraph.substring( paragraph.indexOf( '|extraprefix=' ) + 13, paragraph.indexOf( '|query=' ) ).trim(); - query = prefix + '\n\n' + query; - } - - if ( title ) { - exampleQueries.add( new Option( title, query ) ); - } - } - } ) - } ); - } - - /** - * Add query example to editor window. - */ - function pasteExample() { - var text = this.value; - this.selectedIndex = 0; - if ( !text || !text.trim() ) { - return; - } - EDITOR.setValue( text ); - addPrefixes(); - } - - /** - * Setup event handlers. - */ - function setupHandlers() { - $( '#query-form' ).submit( submitQuery ); - $( '.namespace-shortcuts' ).on( 'change', 'select', selectNamespace ); - $( '.exampleQueries' ).on( 'change', pasteExample ); - $( '.addPrefixes' ).click( addPrefixes ); - $( '#showhide' ).click( showHideHelp ); - $( '#hide-explorer' ).click( hideExplorer ); - $( '#clear-button' ).click( function () { - EDITOR.setValue( '' ); - } ); - for ( format in DOWNLOAD_FORMATS ) { - var extension = DOWNLOAD_FORMATS[format].ext || format.toLowerCase(); - var formatName = format.replace( /\s/g, '-' ); - $( '#download' + formatName ).click( downloadHandler( 'query.' + extension, - DOWNLOAD_FORMATS[format].handler, DOWNLOAD_FORMATS[format].mimetype - ) ); - } - } - - /** - * Create download handler function. - * - * @param {string} filename - * @param {Function} handler - * @param {string} mimetype - * @return {Function} - */ - function downloadHandler( filename, handler, mimetype ) { - return function ( e ) { - e.preventDefault(); - if ( !LAST_RESULT ) { - return ''; - } - download( filename, handler( LAST_RESULT ), mimetype ); - } - } - - /** - * Fetch last DB update time. - */ - function getDbUpdated() { - var query = encodeURI( 'prefix schema: <http://schema.org/> ' - + 'SELECT * WHERE {<http://www.wikidata.org> schema:dateModified ?y}' ); - var url = SERVICE + '?query=' + query, - settings = { - headers: { - 'Accept': 'application/sparql-results+json' - }, - success: showDbQueryResults, - error: dbQueryResultsError - }; - $.ajax( url, settings ); - } - - /** - * Show results for last DB update time. - * - * @param {Object} data - */ - function showDbQueryResults( data ) { - try { - var updateDate = new Date( data.results.bindings[0][data.head.vars[0]].value ); - $( '#dbUpdated' ).text( updateDate.toLocaleTimeString( - navigator.language, - { timeZoneName: 'short' } - ) + ', ' + updateDate.toLocaleDateString( - navigator.language, - { month: 'short', day: 'numeric', year: 'numeric' } - ) ); - } catch ( err ) { - $( '#dbUpdated' ).text( '[unable to connect]' ); - } - } - - /** - * Show error for last DB update time. - * - * @param {Object} jqXHR - * @param {string} textStatus - * @param {string} errorThrown - */ - function dbQueryResultsError( jqXHR, textStatus, errorThrown ) { - $( '#dbUpdated' ).text( '[unable to connect]' ); - } - - /** - * Initialize GUI - */ - function startGui() { - setupEditor(); - setupExamples(); - populateNamespaceShortcuts(); - setupHandlers(); - initQuery(); - getDbUpdated(); - } - - /** - * Process SPARQL query result. - * - * @param {Object} data - * @param {Function} rowHandler - * @param {*} context - * @return {*} The provided context, modified by the rowHandler. - */ - function processData( data, rowHandler, context ) { - var results = data.results.bindings.length; - for ( var i = 0; i < results; i++ ) { - var rowBindings = {}; - for ( var j = 0; j < data.head.vars.length; j++ ) { - if ( data.head.vars[j] in data.results.bindings[i] ) { - rowBindings[data.head.vars[j]] = data.results.bindings[i][data.head.vars[j]]; - } - } - context = rowHandler( rowBindings, context ); - } - return context; - } - - /** - * Encode string as CSV. - * - * @param {string} string - * @return {string} - */ - function encodeCsv( string ) { - var result = string.replace( /"/g, '""' ); - if ( result.search( /("|,|\n)/g ) >= 0 ) { - result = '"' + result + '"'; - } - return result; - } - - /** - * Get CSV rendering of the result data. - * - * @param {Object} data - * @return {string} - */ - function getCsvData( data ) { - var out = data.head.vars.map( encodeCsv ).join( ',' ) + '\n'; - out = processData( data, function ( row, out ) { - var rowOut = ''; - for ( rowVar in row ) { - var rowCSV = encodeCsv( row[rowVar].value ); - if ( rowOut.length > 0 ) { - rowOut += ','; - } - rowOut += rowCSV; - } - if ( rowOut.length > 0 ) { - rowOut += '\n'; - } - return out + rowOut; - }, out ); - return out; - } - - /** - * Get TSV rendering of the result data. - * - * @param {Object} data - * @return {string} - */ - function getSimpleTsvData( data ) { - var out = data.head.vars.join( '\t' ) + '\n'; - out = processData( data, function ( row, out ) { - var rowOut = ''; - for ( rowVar in row ) { - var rowTSV = row[rowVar].value.replace( /\t/g, '' ); - if ( rowOut.length > 0 ) { - rowOut += '\t'; - } - rowOut += rowTSV; - } - if ( rowOut.length > 0 ) { - rowOut += '\n'; - } - return out + rowOut; - }, out ); - return out; - } - - /** - * Render value as per http://www.w3.org/TR/sparql11-results-csv-tsv/#tsv - * - * @param {Object} binding - * @return {string} - */ - function renderValueTSV( binding ) { - var value = binding.value.replace( /\t/g, '' ); - switch ( binding.type ) { - case 'uri': - return '<' + value + '>'; - case 'bnode': - return '_:' + value; - case 'literal': - var lvalue = JSON.stringify( value ); - if ( binding['xml:lang'] ) { - return lvalue + '@' + binding['xml:lang']; - } - if ( binding.datatype ) { - if ( binding.datatype === 'http://www.w3.org/2001/XMLSchema#integer' || - binding.datatype === 'http://www.w3.org/2001/XMLSchema#decimal' || - binding.datatype === 'http://www.w3.org/2001/XMLSchema#double' - ) { - return value; - } - return lvalue + '^^<' + binding.datatype + '>'; - } - return lvalue; - } - return value; - } - - /** - * Get TSV rendering of the result data according to SPARQL standard. - * See: http://www.w3.org/TR/sparql11-results-csv-tsv/#tsv - * - * @param {Object} data - * @return {string} - */ - function getSparqlTsvData( data ) { - var out = data.head.vars.map( function ( vname ) { - return '?' + vname; - } ).join( '\t' ) + '\n'; - out = processData( data, function ( row, out ) { - var rowOut = ''; - for ( rowVar in row ) { - var rowTSV = renderValueTSV( row[rowVar] ); - if ( rowOut.length > 0 ) { - rowOut += '\t'; - } - rowOut += rowTSV; - } - if ( rowOut.length > 0 ) { - rowOut += '\n'; - } - return out + rowOut; - }, out ); - return out; - } - - /** - * Get JSON rendering of the result data. - * - * @param {Object} data - * @return {string} - */ - function getJsonData( data ) { - var out = []; - out = processData( data, function ( row, out ) { - var extractRow = {}; - for ( rowVar in row ) { - extractRow[rowVar] = row[rowVar].value; - } - out.push( extractRow ); - return out; - }, out ); - return JSON.stringify( out ); - } - - /** - * @param {Object} data - * @returns {string} - */ - function getAllJsonData( data ) { - return JSON.stringify( data ); - } - - /** - * Produce file download. - * - * @param {string} filename - * @param {string} text - * @param {string} contentType - */ - function download( filename, text, contentType ) { - if ( !text ) { - return; - } - var element = document.createElement( 'a' ); - element.setAttribute( 'href', 'data:' + contentType + ',' + encodeURIComponent( text ) ); - element.setAttribute( 'download', filename ); - - element.style.display = 'none'; - document.body.appendChild( element ); - element.click(); - document.body.removeChild( element ); - } - - $( document ).ready( function () { - startGui(); - } ); - $( window ).on( 'popstate', initQuery ); -} )( jQuery, mediaWiki ); diff --git a/gui/index.html b/gui/index.html index c98cf70..db2efc2 100644 --- a/gui/index.html +++ b/gui/index.html @@ -10,10 +10,18 @@ <script src="wikibase/codemirror/addon/hint/wikibase-sparql-hint.js"></script> <script src="wikibase/codemirror/addon/hint/wikibase-rdf-hint.js"></script> <script src="wikibase/codemirror/addon/tooltip/WikibaseRDFTooltip.js"></script> -<script src="gui.js"></script> + +<script src="wikibase/queryService/ui/App.js"></script> +<script src="wikibase/queryService/ui/Editor.js"></script> +<script src="wikibase/queryService/api/Sparql.js"></script> +<script src="wikibase/queryService/api/QuerySamples.js"></script> +<script src="wikibase/queryService/RdfNamespaces.js"></script> +<script src="wikibase/init.js"></script> + <script src="vis.js"></script> <script src="wdqs.js"></script> <script src="wdqs-explorer.js"></script> + <link rel="stylesheet" href="vendor/bootstrap/bootstrap.min.css"> <link rel="stylesheet" href="vendor/codemirror/lib/codemirror.css"> <link rel="stylesheet" href="vendor/codemirror/addon/hint/show-hint.css"> @@ -24,7 +32,7 @@ <title>Wikidata Query Service (Beta)</title> </head> <body> - <div class="container-fluid"> + <div class="wikibase-queryservice container-fluid"> <div class="row"> <!-- Editor Navbar --> <nav class="navbar navbar-default"> @@ -55,7 +63,7 @@ <li><a target="_blank" href="https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format">RDF Data Model</a></li> <li><a target="_blank" href="https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Full_list_of_prefixes">List of prefixes</a></li> </ul></li> - <li><select id="exampleQueries" class="exampleQueries form-control navbar-btn"><option value="">Query Examples</option></select></li> + <li><select class="exampleQueries form-control navbar-btn"><option value="">Query Examples</option></select></li> </ul> </div> </nav> @@ -63,7 +71,7 @@ <div class="col-md-12" id="query-box"> <form class="form-horizontal" id="query-form"> <div class="form-group"> - <textarea id="query" name="query" placeholder="(Input a SPARQL query or choose a query example)"></textarea> + <textarea class="queryEditor" name="query" placeholder="(Input a SPARQL query or choose a query example)"></textarea> <div class="alert alert-success alert-dismissible" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button> Press <i>[CTRL-SPACE]</i> to activate auto completion. Data last updated: <span id="dbUpdated">[connecting]</span> @@ -82,8 +90,7 @@ <div class="row" id="total"> <div class="col-md-4 pull-left"> Total results: <span id="total-results"></span>, duration: <span - id="query-time"></span> ms. <span id="show-explorer">Click - on <b>*</b> to explore. + id="query-time"></span> ms. <span id="show-explorer"> </span> <button id="hide-explorer" href="#">Hide explorer</button> </div> diff --git a/gui/js/wikibase/queryService/Api.js b/gui/js/wikibase/queryService/Api.js deleted file mode 100644 index 7462900..0000000 --- a/gui/js/wikibase/queryService/Api.js +++ /dev/null @@ -1,44 +0,0 @@ -var wikibase = wikibase || { queryService: { ui: {}} }; - -wikibase.queryService.Api = ( function( $ ) { - "use strict"; - - /** - * API for the Wikibase query service - * - * @class wikibase.queryService.Api - * @licence GNU GPL v2+ - * @author Jonas Kress - * @constructor - */ - function SELF() { - } - - /** - * Submit a query to the API - * - * @param {string[]} data - * @return {jQuery.Promise} - **/ - SELF.prototype.submitQuery = function( data ){ - }; - - /** - * Get the result of the submitted query - * - * @return {string} - **/ - SELF.prototype.getResult = function(){ - }; - - /** - * Get the result of the submitted query as CSV - * - * @return {string} - **/ - SELF.prototype.getResultAsCSV = function(){ - }; - - return SELF; - -}( jQuery ) ); diff --git a/gui/js/wikibase/queryService/ui/App.js b/gui/js/wikibase/queryService/ui/App.js deleted file mode 100644 index 705d559..0000000 --- a/gui/js/wikibase/queryService/ui/App.js +++ /dev/null @@ -1,53 +0,0 @@ -var wikibase = wikibase || { queryService: { ui: {}} }; - -wikibase.queryService.ui.App = ( function( $ ) { - "use strict"; - - /** - * A ui application for the Wikibase query service - * - * @class wikibase.queryService.ui.App - * @licence GNU GPL v2+ - * @author Jonas Kress - * @constructor - * - * @param {jQuery} $element - * @param {wikibase.queryService.ui.Editor} - * @param {wikibase.queryService.API} - */ - function SELF( $element, editor, api ) { - this.$element = $element; - this.editor = editor; - this.api = api; - - if( !this.api ){ - this.api = new wikibase.queryService.Api(); - } - - if( !this.editor ){ - this.editor = new wikibase.queryService.ui.Editor(); - } - } - - /** - * @property {string} - * @private - **/ - this.$element = null; - - /** - * @property {wikibase.queryService.API} - * @private - **/ - this.api = null; - - /** - * @property {wikibase.queryService.ui.Editor} - * @type wikibase.queryService.ui.Editor - * @private - **/ - this.editor = null; - - - return SELF; -}( jQuery ) ); diff --git a/gui/js/wikibase/queryService/ui/Editor.js b/gui/js/wikibase/queryService/ui/Editor.js deleted file mode 100644 index 30bae83..0000000 --- a/gui/js/wikibase/queryService/ui/Editor.js +++ /dev/null @@ -1,29 +0,0 @@ -var wikibase = wikibase || { queryService: { ui: {}} }; - -wikibase.queryService.ui.Editor = ( function( $ ) { - "use strict"; - - /** - * An ui editor for the Wikibase query service - * - * @class wikibase.queryService.ui.Editor - * @licence GNU GPL v2+ - * @author Jonas Kress - * @constructor - */ - function SELF() { - } - - /** - * Construct an editor on the given textarea DOM element - * - * @param {jQuery} $element - * @throws {Error} If given element is not a valid jQuery textarea - **/ - SELF.prototype.fromTextArea = function( $element ){ - }; - - - return SELF; - -}( jQuery ) ); diff --git a/gui/style.css b/gui/style.css index 039611c..0064d22 100644 --- a/gui/style.css +++ b/gui/style.css @@ -16,7 +16,7 @@ } .error-line { - background: red; + border-bottom: 2px dotted red; } .error-character { diff --git a/gui/wikibase/init.js b/gui/wikibase/init.js new file mode 100644 index 0000000..7922f7c --- /dev/null +++ b/gui/wikibase/init.js @@ -0,0 +1,4 @@ +$( document ).ready( function () { + new wikibase.queryService.ui.App($('.wikibase-queryservice ')); +} ); + diff --git a/gui/wikibase/queryService/RdfNamespaces.js b/gui/wikibase/queryService/RdfNamespaces.js new file mode 100644 index 0000000..2bf73b2 --- /dev/null +++ b/gui/wikibase/queryService/RdfNamespaces.js @@ -0,0 +1,56 @@ +var wikibase = wikibase || {}; +wikibase.queryService = wikibase.queryService || {}; + +wikibase.queryService.RdfNamespaces = {}; + +(function(RdfNamespaces){ + + RdfNamespaces.NAMESPACE_SHORTCUTS = { + 'Wikidata' : { + 'wikibase': 'http://wikiba.se/ontology#', + 'wd': 'http://www.wikidata.org/entity/', + 'wdt': 'http://www.wikidata.org/prop/direct/', + 'wds': 'http://www.wikidata.org/entity/statement/', + 'p': 'http://www.wikidata.org/prop/', + 'wdref': 'http://www.wikidata.org/reference/', + 'wdv': 'http://www.wikidata.org/value/', + 'ps': 'http://www.wikidata.org/prop/statement/', + 'psv': 'http://www.wikidata.org/prop/statement/value/', + 'pq': 'http://www.wikidata.org/prop/qualifier/', + 'pqv': 'http://www.wikidata.org/prop/qualifier/value/', + 'pr': 'http://www.wikidata.org/prop/reference/', + 'prv': 'http://www.wikidata.org/prop/reference/value/', + 'wdno': 'http://www.wikidata.org/prop/novalue/', + 'wdata': 'http://www.wikidata.org/wiki/Special:EntityData/' + }, + 'W3C' : { + 'rdfs' : 'http://www.w3.org/2000/01/rdf-schema#', + 'rdf' : 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'owl' : 'http://www.w3.org/2002/07/owl#', + 'skos' : 'http://www.w3.org/2004/02/skos/core#', + 'xsd' : 'http://www.w3.org/2001/XMLSchema#', + 'prov' : 'http://www.w3.org/ns/prov#' + }, + 'Social/Other' : { + 'schema' : 'http://schema.org/' + }, + 'Blazegraph' : { + 'bd' : 'http://www.bigdata.com/rdf#', + 'bds' : 'http://www.bigdata.com/rdf/search#', + 'gas' : 'http://www.bigdata.com/rdf/gas#', + 'hint' : 'http://www.bigdata.com/queryHints#' + } + }; + + RdfNamespaces.STANDARD_PREFIXES = [ + 'PREFIX wd: <http://www.wikidata.org/entity/>', + 'PREFIX wdt: <http://www.wikidata.org/prop/direct/>', + 'PREFIX wikibase: <http://wikiba.se/ontology#>', + 'PREFIX p: <http://www.wikidata.org/prop/>', + 'PREFIX v: <http://www.wikidata.org/prop/statement/>', + 'PREFIX q: <http://www.wikidata.org/prop/qualifier/>', + 'PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>' + ]; + +})(wikibase.queryService.RdfNamespaces); + diff --git a/gui/wikibase/queryService/api/QuerySamples.js b/gui/wikibase/queryService/api/QuerySamples.js new file mode 100644 index 0000000..c3417ec --- /dev/null +++ b/gui/wikibase/queryService/api/QuerySamples.js @@ -0,0 +1,68 @@ +var wikibase = wikibase || {}; +wikibase.queryService = wikibase.queryService || {}; +wikibase.queryService.api = wikibase.queryService.api || {}; + +wikibase.queryService.api.QuerySamples = ( function( $ ) { + "use strict"; + + /** + * QuerySamples API for the Wikibase query service + * + * @class wikibase.queryService.api.QuerySamples + * @licence GNU GPL v2+ + * + * @author Stanislav Malyshev + * @author Jonas Kress + * @constructor + */ + function SELF() { + } + + /** + * @return {jQuery.Deferred} Object taking list of example queries { title:, query: } + **/ + SELF.prototype.getExamples = function() { + + var examples = {}, deferred = $.Deferred(); + + $.ajax( { + url: 'https://www.mediawiki.org/w/api.php?action=query&prop=revisions&titles=Wikibase/' + + 'Indexing/SPARQL_Query_Examples&rvprop=content', + data: { + format: 'json' + }, + dataType: 'jsonp' + } ).done( function ( data ) { + var wikitext = data.query.pages[Object.keys( data.query.pages )].revisions[0]['*']; + var paragraphs = wikitext.split( '==' ); + + //TODO extract and make more robust against wrong formatting on the wiki page + $.each( paragraphs, function ( key, paragraph ) { + if ( paragraph.match( /SPARQL\|.*query\=/ ) ) { + var query = paragraph.substring( + paragraph.indexOf( '|query=' ) + 7, + paragraph.lastIndexOf( '}}' ) + ).trim(); + var title = paragraphs[key - 1] || ''; + title = title.replace( '=', '' ).trim(); + + if ( paragraph.match( 'extraprefix=' ) ) { + var prefix = paragraph.substring( paragraph.indexOf( '|extraprefix=' ) + 13, paragraph.indexOf( '|query=' ) ).trim(); + query = prefix + '\n\n' + query; + } + + if ( title ) { + examples[title] = query; + } + } + } ); + + deferred.resolve( examples ); + } ); + + return deferred; + }; + + return SELF; + +}( jQuery ) ); diff --git a/gui/wikibase/queryService/api/Sparql.js b/gui/wikibase/queryService/api/Sparql.js new file mode 100644 index 0000000..2de8db1 --- /dev/null +++ b/gui/wikibase/queryService/api/Sparql.js @@ -0,0 +1,425 @@ +var wikibase = wikibase || {}; +wikibase.queryService = wikibase.queryService || {}; +wikibase.queryService.api = wikibase.queryService.api || {}; + +wikibase.queryService.api.Sparql = (function($) { + "use strict"; + + var SERVICE = '/bigdata/namespace/wdq/sparql'; + + /** + * SPARQL API for the Wikibase query service + * + * @class wikibase.queryService.api.Sparql + * @licence GNU GPL v2+ + * + * @author Stanislav Malyshev + * @author Jonas Kress + * @constructor + */ + function SELF() { + } + + /** + * @property {int} + * @private + **/ + SELF.prototype._executionTime = null; + + /** + * @property {string} + * @private + **/ + SELF.prototype._errorMessage = null; + + /** + * @property {int} + * @private + **/ + SELF.prototype._resultLength = null; + + /** + * @property {Object} + * @private + **/ + SELF.prototype._rawData = null; + + /** + * Submit a query to the API + * + * @return {jQuery.Promise} + */ + SELF.prototype.queryDataUpdatedTime = function() { + + var deferred = $.Deferred(), + query = encodeURI('prefix schema: <http://schema.org/> ' + + 'SELECT * WHERE {<http://www.wikidata.org> schema:dateModified ?y}'), + url = SERVICE + '?query=' + query, settings = { + 'headers' : { + 'Accept' : 'application/sparql-results+json' + }, + }; + + $.ajax( url, settings ).done(function( data ){ + var updateDate = new Date( data.results.bindings[0][data.head.vars[0]].value ), + dateText = updateDate.toLocaleTimeString( + navigator.language, + { timeZoneName: 'short' } + ) + ', ' + updateDate.toLocaleDateString( + navigator.language, + { month: 'short', day: 'numeric', year: 'numeric' } + ); + + deferred.resolve( dateText ); + }).fail(function(){ + deferred.reject(); + }); + + return deferred; + }; + + /** + * Submit a query to the API + * + * @param {string[]} + * query + * @return {jQuery.Promise} + */ + SELF.prototype.query = function(query) { + var deferred = $.Deferred(), self = this; + + var url = SERVICE + '?' + query, settings = { + 'headers' : { + 'Accept' : 'application/sparql-results+json' + }, + }; + + this._executionTime = Date.now(); + $.ajax( url, settings ).done(function( data, textStatus, jqXHR ) { + self._executionTime = Date.now() - self._executionTime; + self._resultLength = data.results.bindings.length || null; + self._rawData = data; + + deferred.resolve(); + }).fail(function( jqXHR, textStatus, errorThrown ) { + self._executionTime = null; + self._rawData = null; + self._resultLength = null; + self._generateErrorMessage(jqXHR); + + deferred.reject(); + }); + + return deferred; + }; + + /** + * Get execution time in ms of the submitted query + * + * @return {int} + */ + SELF.prototype._generateErrorMessage = function( jqXHR ) { + var message = 'ERROR: '; + + if ( jqXHR.status === 0 ) { + message += 'Could not contact server'; + } else { + message += jqXHR.responseText; + if ( jqXHR.responseText.match( /Query deadline is expired/ ) ) { + message = 'QUERY TIMEOUT\n' + message; + } + } + this._errorMessage = message; + }; + + /** + * Produce abbreviation of the URI. + * + * @param {string} uri + * @returns {string} + */ + SELF.prototype.abbreviateUri = function( uri ) { + var nsGroup, ns, NAMESPACE_SHORTCUTS = wikibase.queryService.RdfNamespaces.NAMESPACE_SHORTCUTS; + + for ( nsGroup in NAMESPACE_SHORTCUTS ) { + for ( ns in NAMESPACE_SHORTCUTS[nsGroup] ) { + if ( uri.indexOf( NAMESPACE_SHORTCUTS[nsGroup][ns] ) === 0 ) { + return uri.replace( NAMESPACE_SHORTCUTS[nsGroup][ns], ns + ':' ); + } + } + } + return '<' + uri + '>'; + }; + + /** + * Get execution time in seconds of the submitted query + * + * @return {int} + */ + SELF.prototype.getExecutionTime = function() { + return this._executionTime; + }; + + /** + * Get error message of the submitted query if it has failed + * + * @return {int} + */ + SELF.prototype.getErrorMessage = function() { + return this._errorMessage; + }; + + /** + * Get result length of the submitted query if it has failed + * + * @return {int} + */ + SELF.prototype.getResultLength = function() { + return this._resultLength; + }; + + /** + * Get the result as table + * + * @return {jQuery} + */ + SELF.prototype.getResultAsTable = function() { + var data = this._rawData, thead, tr, td, binding, + table = $( '<table>' ).attr( 'class', 'table' ); + + if ( typeof data.boolean !== 'undefined' ) { + // ASK query + table.append( '<tr><td>' + data.boolean + '</td></tr>' ).addClass( 'boolean' ); + return; + } + + thead = $( '<thead>' ).appendTo( table ); + tr = $( '<tr>' ); + for ( i = 0; i < data.head.vars.length; i++ ) { + tr.append( '<th>' + data.head.vars[i] + '</th>' ); + } + thead.append( tr ); + table.append( thead ); + + for (var i = 0; i < this._resultLength; i++ ) { + tr = $( '<tr>' ) ; + for ( var j = 0; j < data.head.vars.length; j++ ) { + td = $( '<td>' ) ; + if ( data.head.vars[j] in data.results.bindings[i] ) { + binding = data.results.bindings[i][data.head.vars[j]]; + var text = binding.value; + if ( binding.type === 'uri' ) { + text = this.abbreviateUri( text ); + } + var linkText = $( '<pre>' ).text( text.trim() ); + if ( binding.type === 'typed-literal' ) { + td.attr( { + 'class': 'literal', + 'data-datatype': binding.datatype + } ).append( linkText ); + } else { + td.attr( 'class', binding.type ); + if ( binding.type === 'uri' ) { + td.append( $( '<a>' ) + .attr( 'href', binding.value ) + .append( linkText ) + ); + } else { + td.append( linkText ); + } + + if ( binding['xml:lang'] ) { + td.attr( { + 'data-lang': binding['xml:lang'], + title: binding.value + '@' + binding['xml:lang'] + } ); + } + } + } else { + // no binding + td.attr( 'class', 'unbound' ); + } + tr.append( td ); + } + table.append( tr ); + } + + return table; + }; + + /** + * Process SPARQL query result. + * + * @param {Object} data + * @param {Function} rowHandler + * @param {*} context + * @private + * @return {*} The provided context, modified by the rowHandler. + */ + SELF.prototype._processData = function( data, rowHandler, context ) { + var results = data.results.bindings.length; + for ( var i = 0; i < results; i++ ) { + var rowBindings = {}; + for ( var j = 0; j < data.head.vars.length; j++ ) { + if ( data.head.vars[j] in data.results.bindings[i] ) { + rowBindings[data.head.vars[j]] = data.results.bindings[i][data.head.vars[j]]; + } + } + context = rowHandler( rowBindings, context ); + } + return context; + }; + + /** + * Encode string as CSV. + * + * @param {string} string + * @return {string} + */ + SELF.prototype._encodeCsv = function( string ) { + var result = string.replace( /"/g, '""' ); + if ( result.search( /("|,|\n)/g ) >= 0 ) { + result = '"' + result + '"'; + } + return result; + }; + + /** + * Get the result of the submitted query as CSV + * + * @return {string} csv + */ + SELF.prototype.getResultAsCsv = function() { + var self = this, data = self._rawData; + var out = data.head.vars.map( this._encodeCsv ).join( ',' ) + '\n'; + out = this._processData( data, function ( row, out ) { + var rowOut = ''; + for ( var rowVar in row ) { + var rowCSV = self._encodeCsv( row[rowVar].value ); + if ( rowOut.length > 0 ) { + rowOut += ','; + } + rowOut += rowCSV; + } + if ( rowOut.length > 0 ) { + rowOut += '\n'; + } + return out + rowOut; + }, out ); + return out; + }; + + /** + * Get the result of the submitted query as JSON + * + * @return {string} + */ + SELF.prototype.getResultAsJson = function() { + var out = [], data = this._rawData; + out = this._processData( data, function ( row, out ) { + var extractRow = {}; + for ( var rowVar in row ) { + extractRow[rowVar] = row[rowVar].value; + } + out.push( extractRow ); + return out; + }, out ); + return JSON.stringify( out ); + }; + + /** + * Get the result of the submitted query as JSON + * + * @return {string} + */ + SELF.prototype.getResultAsAllJson = function() { + return JSON.stringify( this._rawData ); + }; + + /** + * Render value as per http://www.w3.org/TR/sparql11-results-csv-tsv/#tsv + * + * @param {Object} binding + * @return {string} + */ + SELF.prototype._renderValueTSV = function( binding ) { + var value = binding.value.replace( /\t/g, '' ); + switch ( binding.type ) { + case 'uri': + return '<' + value + '>'; + case 'bnode': + return '_:' + value; + case 'literal': + var lvalue = JSON.stringify( value ); + if ( binding['xml:lang'] ) { + return lvalue + '@' + binding['xml:lang']; + } + if ( binding.datatype ) { + if ( binding.datatype === 'http://www.w3.org/2001/XMLSchema#integer' || + binding.datatype === 'http://www.w3.org/2001/XMLSchema#decimal' || + binding.datatype === 'http://www.w3.org/2001/XMLSchema#double' + ) { + return value; + } + return lvalue + '^^<' + binding.datatype + '>'; + } + return lvalue; + } + return value; + }; + + /** + * Get the result of the submitted query as TSV + * + * @return {string} + */ + SELF.prototype.getSparqlTsv = function() { + var data = this._rawData, self = this, + out = data.head.vars.map( function ( vname ) { + return '?' + vname; + } ).join( '\t' ) + '\n'; + out = this._processData( data, function ( row, out ) { + var rowOut = ''; + for ( var rowVar in row ) { + var rowTSV = self._renderValueTSV( row[rowVar] ); + if ( rowOut.length > 0 ) { + rowOut += '\t'; + } + rowOut += rowTSV; + } + if ( rowOut.length > 0 ) { + rowOut += '\n'; + } + return out + rowOut; + }, out ); + return out; + }; + + /** + * Get the result of the submitted query as TSV + * + * @return {string} + */ + SELF.prototype.getSimpleTsv = function() { + var data = this._rawData, + out = data.head.vars.join( '\t' ) + '\n'; + out = this._processData( data, function ( row, out ) { + var rowOut = ''; + for ( var rowVar in row ) { + var rowTSV = row[rowVar].value.replace( /\t/g, '' ); + if ( rowOut.length > 0 ) { + rowOut += '\t'; + } + rowOut += rowTSV; + } + if ( rowOut.length > 0 ) { + rowOut += '\n'; + } + return out + rowOut; + }, out ); + return out; + }; + + return SELF; + +}(jQuery)); diff --git a/gui/wikibase/queryService/ui/App.js b/gui/wikibase/queryService/ui/App.js new file mode 100644 index 0000000..1107902 --- /dev/null +++ b/gui/wikibase/queryService/ui/App.js @@ -0,0 +1,363 @@ +var wikibase = wikibase || {}; +wikibase.queryService = wikibase.queryService || {}; +wikibase.queryService.ui = wikibase.queryService.ui || {}; +window.mediaWiki = window.mediaWiki || {}; + +wikibase.queryService.ui.App = ( function( $, mw ) { + "use strict"; + + var SHORTURL = 'http://tinyurl.com/create.php?url='; + var EXPLORE_URL = 'http://www.wikidata.org/entity/Q'; + + /** + * A ui application for the Wikibase query service + * + * @class wikibase.queryService.ui.App + * @licence GNU GPL v2+ + * + * @author Stanislav Malyshev + * @author Jonas Kress + * @constructor + * + * @param {jQuery} $element + * @param {wikibase.queryService.ui.Editor} + * @param {wikibase.queryService.api.Sparql} + */ + function SELF( $element, editor, sparqlApi, querySamplesApi ) { + + this._$element = $element; + this._editor = editor; + this._sparqlApi = sparqlApi; + this._querySamplesApi = querySamplesApi; + + this._init(); + } + + /** + * @property {string} + * @private + **/ + SELF.prototype._$element = null; + + /** + * @property {wikibase.queryService.api.Sparql} + * @private + **/ + SELF.prototype._sparqlApi = null; + + /** + * @property {wikibase.queryService.api.QuerySamplesApi} + * @private + **/ + SELF.prototype._querySamplesApi = null; + + /** + * @property {wikibase.queryService.ui.Editor} + * @type wikibase.queryService.ui.Editor + * @private + **/ + SELF.prototype._editor = null; + + /** + * Initialize private members and call delegate to specific init methods + * @private + **/ + SELF.prototype._init = function() { + + if( !this._sparqlApi ){ + this._sparqlApi = new wikibase.queryService.api.Sparql(); + } + + if( !this._querySamplesApi ){ + this._querySamplesApi = new wikibase.queryService.api.QuerySamples(); + } + + if( !this._editor ){ + this._editor = new wikibase.queryService.ui.Editor(); + } + + this._initEditor(); + this._initExamples(); + this._initDataUpdated(); + this._initQuery(); + this._initRdfNamespaces(); + this._initHandlers(); + + }; + + /** + * @private + **/ + SELF.prototype._initEditor = function() { + + this._editor.fromTextArea( this._$element.find( '.queryEditor' )[0] ); + this._editor.addKeyMap( 'Ctrl-Enter', $.proxy( this._handleQuerySubmit, this ) ); + }; + + /** + * @private + **/ + SELF.prototype._initExamples = function() { + + var self = this; + this._querySamplesApi.getExamples().done( function( examples ){ + $.each( examples, function( title, query ) { + self._$element.find( '.exampleQueries' ).append( $(new Option( title, query ) ) ); + }); + } ); + }; + + /** + * @private + */ + SELF.prototype._initRdfNamespaces = function() { + var category, select, ns, container = $( '.namespace-shortcuts' ), + namespaces = wikibase.queryService.RdfNamespaces.NAMESPACE_SHORTCUTS; + + container.click( function( e ) { + e.stopPropagation(); + } ); + + // add namespaces to dropdowns + for ( category in namespaces ) { + select = $( '<select>' ) + .attr( 'class', 'form-control' ) + .append( $( '<option>' ).text( category ) ) + .appendTo( container ); + for ( ns in namespaces[category] ) { + select.append( $( '<option>' ).text( ns ).attr( { + value: namespaces[category][ns] + } ) ); + } + } + }; + + /** + * @private + **/ + SELF.prototype._initQuery = function() { + if ( window.location.hash !== '' ) { + this._editor.setValue( decodeURIComponent( window.location.hash.substr( 1 ) ) ); + this._editor.refresh(); + } + }; + + + + /** + * @private + **/ + SELF.prototype._initDataUpdated = function() { + this._sparqlApi.queryDataUpdatedTime().done( function( time ){ + $( '#dbUpdated' ).text( time ); + } ).fail(function(){ + $( '#dbUpdated' ).text( '[unable to connect]' ); + }); + }; + + + /** + * @private + **/ + SELF.prototype._initHandlers = function() { + var self = this; + + $( '#query-form' ).submit( $.proxy( this._handleQuerySubmit, this ) ); + $( '.namespace-shortcuts' ).on( 'change', 'select', $.proxy( this._handleNamespaceSelected, this ) ); + $( '.exampleQueries' ).on( 'change', $.proxy( this._handleExampleSelected, this ) ); + + $( '.addPrefixes' ).click( function() { + var prefixes = wikibase.queryService.RdfNamespaces.STANDARD_PREFIXES.join( '\n' ); + self._editor.prepandValue( prefixes + '\n\n' ); + } ); + + $( '#clear-button' ).click( function () { + self._editor.setValue( '' ); + } ); + + $( '#hide-explorer' ).click( function( e ){ + e.preventDefault(); + $( '#explore' ).empty( '' ); + $( '#hide-explorer' ).hide(); + $( '#show-explorer' ).show(); + } ); + + $( window ).on( 'popstate', $.proxy( this._initQuery(), this ) ); + + this._initHandlersDownloads(); + }; + + /** + * @private + **/ + SELF.prototype._initHandlersDownloads = function() { + + var api = this._sparqlApi; + var DOWNLOAD_FORMATS = { + 'CSV': { + handler: $.proxy( api.getResultAsCsv, api ), + mimetype: 'text/csv;charset=utf-8' + }, + 'JSON': { + handler: $.proxy( api.getResultAsJson, api ), + mimetype: 'application/json;charset=utf-8' + }, + 'TSV': { + handler: $.proxy( api.getSparqlTsv, api ), + mimetype: 'text/tab-separated-values;charset=utf-8' + }, + 'Simple TSV': { + handler: $.proxy( api.getSimpleTsv, api ), + mimetype: 'text/tab-separated-values;charset=utf-8', + ext: 'tsv' + }, + 'Full JSON': { + handler: $.proxy( api.getResultAsAllJson, api ), + mimetype: 'application/json;charset=utf-8', + ext: 'json' + } + }; + + var download = function( filename, handler, mimetype ) { + + return function ( e ) { + e.preventDefault(); + + if ( api.getResultLength() === null ) { + return ''; + } + + var link = document.createElement("a"); + link.download = filename; + link.href = 'data:' + mimetype + ',' + encodeURIComponent( handler() ); + link.click(); + }; + }; + + for ( var format in DOWNLOAD_FORMATS ) { + var extension = DOWNLOAD_FORMATS[format].ext || format.toLowerCase(); + var formatName = format.replace( /\s/g, '-' ); + $( '#download' + formatName ).click( + download( 'query.' + extension, DOWNLOAD_FORMATS[format].handler, + DOWNLOAD_FORMATS[format].mimetype ) + ); + } + }; + + /** + * @private + **/ + SELF.prototype._handleQuerySubmit = function( e ) { + var self = this; + + e.preventDefault(); + this._editor.save(); + + var hash = encodeURIComponent( this._editor.getValue() ); + if ( window.location.hash !== hash ) { + window.location.hash = hash; + } + $( '#shorturl' ).attr( 'href', SHORTURL + encodeURIComponent( window.location ) ); + + $( '#query-result' ).empty( '' ); + $( '#query-result' ).hide(); + $( '#total' ).hide(); + $( '#query-error' ).show(); + $( '#query-error' ).text( 'Running query...' ); + + var query = $( '#query-form' ).serialize(); + this._sparqlApi.query(query) + .done( $.proxy( this._handleQueryResult, this ) ) + .fail(function(){ + $( '#query-error' ).html( $( '<pre>' ).text( self._sparqlApi.getErrorMessage() ) ).show(); + self._editor.highlightError( self._sparqlApi.getErrorMessage() ); + } ); + + }; + + /** + * @private + */ + SELF.prototype._handleQueryResult = function() { + + var api = this._sparqlApi; + $( '#total-results' ).text( api.getResultLength() ); + $( '#query-time' ).text( api.getExecutionTime() ); + $( '#total' ).show(); + $( '#query-result' ).append( api.getResultAsTable() ).show(); + $( '#query-error' ).hide(); + + var $linkableItems = $( '#query-result' ).find('a').filter( function() { + return this.href.match( EXPLORE_URL + '(.+)' ); + } ); + + var $exploreLink = $( '<a href="#" title="Explore item" class="glyphicon glyphicon-search" aria-hidden="true"></a>' ); + $exploreLink.click( $.proxy( this._handleExploreItem, this ) ); + $linkableItems.after( $exploreLink ); + + //$linkableItems.attr( 'href', '#'); + }; + + + /** + * @private + */ + SELF.prototype._handleNamespaceSelected = function( e ) { + var ns, uri = e.target.value, current = this._editor.getValue(); + + if ( current.indexOf( '<' + uri + '>' ) === -1 ) { + ns = $( e.target ).find(':selected').text(); + this._editor.setValue('PREFIX ' + ns + ': <' + uri + '>\n' + current); + } + + // reselect group label + e.target.selectedIndex = 0; + }; + + + /** + * @private + */ + SELF.prototype._handleExampleSelected = function( e ) { + var text = e.target.value; + e.target.selectedIndex = 0; + if ( !text || !text.trim() ) { + return; + } + this._editor.setValue( text ); + + var prefixes = wikibase.queryService.RdfNamespaces.STANDARD_PREFIXES.join( '\n' ); + + this._editor.prepandValue( prefixes + '\n\n' ); + + }; + + /** + * @private + */ + SELF.prototype._handleExploreItem = function( e ) { + var id, url = $(e.target).prev().attr( 'href' ) || '', match; + + match = url.match( EXPLORE_URL + '(.+)' ); + if ( !match ) { + return; + } + + if ( $( '#hide-explorer' ).is( ':visible' ) ) { + $( '#explore' ).empty( '' ); + } else { + $( '#hide-explorer' ).show(); + $( '#show-explorer' ).hide(); + } + id = match[1]; + mw.config = { get: function () { + return 'Q'+id; + } }; + $( 'html, body' ).animate( { scrollTop: $( '#explore' ).offset().top }, 500 ); + EXPLORER( $, mw, $( '#explore' ) ); + + return false; + }; + + return SELF; +}( jQuery, mediaWiki ) ); diff --git a/gui/wikibase/queryService/ui/Editor.js b/gui/wikibase/queryService/ui/Editor.js new file mode 100644 index 0000000..84e195e --- /dev/null +++ b/gui/wikibase/queryService/ui/Editor.js @@ -0,0 +1,123 @@ +var wikibase = wikibase || {}; +wikibase.queryService = wikibase.queryService || {}; +wikibase.queryService.ui = wikibase.queryService.ui || {}; + +wikibase.queryService.ui.Editor = ( function( $ ) { + "use strict"; + + var CODEMIRROR_DEFAULTS = { + "lineNumbers": true, + "matchBrackets": true, + "mode": 'sparql', + "extraKeys": { 'Ctrl-Space': 'autocomplete' }, + "viewportMargin": Infinity + }, + ERROR_LINE_MARKER = null, + ERROR_CHARACTER_MARKER = null; + + /** + * An ui this._editor for the Wikibase query service + * + * @class wikibase.queryService.ui.this._editor + * @licence GNU GPL v2+ + * + * @author Stanislav Malyshev + * @author Jonas Kress + * @constructor + */ + function SELF() { + } + + /** + * @property {CodeMirror} + * @type CodeMirror + * @private + **/ + SELF.prototype._editor = null; + + /** + * Construct an this._editor on the given textarea DOM element + * + * @param {Element} element + **/ + SELF.prototype.fromTextArea = function( element ) { + + this._editor = CodeMirror.fromTextArea( element, CODEMIRROR_DEFAULTS ); + this._editor.on( 'change', function () { + if ( ERROR_LINE_MARKER ) { + ERROR_LINE_MARKER.clear(); + ERROR_CHARACTER_MARKER.clear(); + } + } ); + this._editor.focus(); + + new WikibaseRDFTooltip(this._editor); + }; + + /** + * Construct an this._editor on the given textarea DOM element + * + * @param {string} keyMap + * @throws {function} callback + **/ + SELF.prototype.addKeyMap = function( keyMap, callback ) { + this._editor.addKeyMap( { keyMap : callback } ); + }; + + /** + * @param {string} value + **/ + SELF.prototype.setValue = function( value ) { + this._editor.setValue( value ); + }; + + /** + * @return {string} + **/ + SELF.prototype.getValue = function() { + return this._editor.getValue(); + }; + + SELF.prototype.save = function() { + this._editor.save(); + }; + + /** + * @param {string} value + **/ + SELF.prototype.prepandValue = function( value ) { + this._editor.setValue( value + this._editor.getValue() ); + }; + + SELF.prototype.refresh = function() { + this._editor.refresh(); + }; + + /** + * Highlight SPARQL error in editor window. + * + * @param {string} description + */ + SELF.prototype.highlightError = function( description ) { + var line, character, + match = description.match( /line (\d+), column (\d+)/ ); + if ( match ) { + // highlight character at error position + line = match[1] - 1; + character = match[2] - 1; + ERROR_LINE_MARKER = this._editor.doc.markText( + { 'line': line, 'ch': 0 }, + { 'line': line }, + { 'className': 'error-line' } + ); + ERROR_CHARACTER_MARKER = this._editor.doc.markText( + { 'line': line, 'ch': character }, + { 'line': line, 'ch': character + 1 }, + { 'className': 'error-character' } + ); + } + }; + + return SELF; + +}( jQuery ) ); -- To view, visit https://gerrit.wikimedia.org/r/253635 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I771c0113503c97c14a4fc6b210ad7e2226570261 Gerrit-PatchSet: 6 Gerrit-Project: wikidata/query/rdf Gerrit-Branch: master Gerrit-Owner: Jonas Kress (WMDE) <jonas.kr...@wikimedia.de> Gerrit-Reviewer: JanZerebecki <jan.wikime...@zerebecki.de> Gerrit-Reviewer: Jonas Kress (WMDE) <jonas.kr...@wikimedia.de> Gerrit-Reviewer: Lydia Pintscher <lydia.pintsc...@wikimedia.de> Gerrit-Reviewer: Smalyshev <smalys...@wikimedia.org> Gerrit-Reviewer: Thiemo Mättig (WMDE) <thiemo.maet...@wikimedia.de> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits