From 6dd7e46c1913b6555d8ac2dd0ef8385d0e214709 Mon Sep 17 00:00:00 2001 From: thomas-topway-it Date: Sat, 30 Mar 2024 16:53:46 +0400 Subject: [PATCH] Datatables v2.1 (#801) * reorganize code * +searchBuilder * +SearchBuilder * fix searchBuilder * fix searchBuilder * add object_hash * Add files via upload * fix use DIWikiPage * fix position sticky * fix searchBuilderType * add datatables.mark * add datatables.mark * add mark, spinner, fix keyword * add mark, spinner * fix context * fix test * fix spinner hide * prevents double spinner * fix regex * fix delimiter --------- Co-authored-by: Bernhard Krabina --- Resources.php | 4 + formats/datatables/Api.php | 127 ++- formats/datatables/DataTables.php | 941 ++++-------------- formats/datatables/SearchPanes.php | 746 ++++++++++++++ .../resources/ext.srf.formats.datatables.css | 11 + .../resources/ext.srf.formats.datatables.js | 335 ++++--- .../jquery/datatables/datatables.mark.min.css | 1 + .../jquery/datatables/datatables.mark.min.js | 7 + .../jquery/datatables/jquery.mark.min.js | 13 + resources/jquery/datatables/object_hash.js | 1 + .../JSONScript/TestCases/datatables-01.json | 2 +- 11 files changed, 1223 insertions(+), 965 deletions(-) create mode 100644 formats/datatables/SearchPanes.php create mode 100644 resources/jquery/datatables/datatables.mark.min.css create mode 100644 resources/jquery/datatables/datatables.mark.min.js create mode 100644 resources/jquery/datatables/jquery.mark.min.js create mode 100644 resources/jquery/datatables/object_hash.js diff --git a/Resources.php b/Resources.php index 57302cc47..8fa5a6638 100644 --- a/Resources.php +++ b/Resources.php @@ -1017,10 +1017,14 @@ 'ext.srf.datatables.v2.module' => $moduleTemplate + [ 'scripts' => [ + 'resources/jquery/datatables/object_hash.js', + 'resources/jquery/datatables/jquery.mark.min.js', + 'resources/jquery/datatables/datatables.mark.min.js', 'resources/jquery/datatables/datatables.min.js', 'resources/jquery/datatables/jquery.dataTables.extras.js', ], 'styles' => [ + 'resources/jquery/datatables/datatables.mark.min.css', 'resources/jquery/datatables/datatables.min.css', ] ], diff --git a/formats/datatables/Api.php b/formats/datatables/Api.php index d66fc2c51..ebfa16067 100644 --- a/formats/datatables/Api.php +++ b/formats/datatables/Api.php @@ -31,9 +31,11 @@ public function execute() { // get request parameters $requestParams = $this->extractRequestParams(); + $data = json_decode( $requestParams['data'], true ); + // @see https://datatables.net/reference/option/ajax - $datatableData = json_decode( $requestParams['datatable'], true ); - $settings = json_decode( $requestParams['settings'], true ); + $datatableData = $data['datatableData']; + $settings = $data['settings']; if ( empty( $datatableData['length'] ) ) { $datatableData['length'] = $settings['defer-each']; @@ -59,7 +61,7 @@ public function execute() { $parameters[$def->getName()] = $def->getDefault(); } - $printoutsRaw = json_decode( $requestParams['printouts'], true ); + $printoutsRaw = $data['printouts']; // add/set specific parameters for this call $parameters = array_merge( @@ -67,7 +69,6 @@ public function execute() { [ // *** important !! 'format' => 'datatables', - "apicall" => "apicall", // @see https://datatables.net/manual/server-side // array length will be sliced client side if greater @@ -104,10 +105,10 @@ public function execute() { foreach ( $printoutsRaw as $printoutData ) { // create property from property key - if ( $printoutData[0] === SMWPrintRequest::PRINT_PROP ) { - $data = $dataValueFactory->newPropertyValueByLabel( $printoutData[1] ); + if ( $printoutData[0] === SMWPrintRequest::PRINT_PROP ) { + $data_ = $dataValueFactory->newPropertyValueByLabel( $printoutData[1] ); } else { - $data = null; + $data_ = null; if ( $hasMainlabel && trim( $parameters['mainlabel'] ) === '-' ) { continue; } @@ -118,7 +119,7 @@ public function execute() { $printouts[] = new SMWPrintRequest( $printoutData[0], // mode $printoutData[1], // (canonical) label - $data, // property name + $data_, // property name $printoutData[3], // output format $printoutData[4] // parameters ); @@ -127,8 +128,8 @@ public function execute() { // SMWQueryProcessor::addThisPrintout( $printouts, $parameters ); - $printrequests = json_decode( $requestParams['printrequests'], true ); - $columnDefs = json_decode( $requestParams['columndefs'], true ); + $printrequests = $data['printrequests']; + $columnDefs = $data['columnDefs']; $getColumnAttribute = function( $label, $attr ) use( $columnDefs ) { foreach ( $columnDefs as $value ) { @@ -171,13 +172,74 @@ public function execute() { } } + // @see https://datatables.net/extensions/searchbuilder/customConditions.html + // @see https://datatables.net/reference/option/searchBuilder.depthLimit + if ( !empty( $datatableData['searchBuilder'] ) ) { + $searchBuilder = []; + foreach ( $datatableData['searchBuilder']['criteria'] as $criteria ) { + foreach ( $printoutsRaw as $key => $value ) { + // @FIXME $label isn't simply $value[1] ? + $printrequest = $printrequests[$key]; + $label = ( $printrequest['key'] !== '' ? $value[1] : '' ); + if ( $label === $criteria['data'] ) { + + // nested condition, skip for now + if ( !array_key_exists( 'condition', $criteria ) ) { + continue; + } + $v = implode( $criteria['value'] ); + $str = ( $label !== '' ? "$label::" : '' ); + switch( $criteria['condition'] ) { + case '=': + $searchBuilder[] = "[[{$str}{$v}]]"; + break; + case '!=': + $searchBuilder[] = "[[{$str}!~$v]]"; + break; + case 'starts': + $searchBuilder[] = "[[{$str}~$v*]]"; + break; + case '!starts': + $searchBuilder[] = "[[{$str}!~$v*]]"; + break; + case 'contains': + $searchBuilder[] = "[[{$str}~*$v*]]"; + break; + case '!contains': + $searchBuilder[] = "[[{$str}!~*$v*]]"; + break; + case 'ends': + $searchBuilder[] = "[[{$str}~*$v]]"; + break; + case '!ends': + $searchBuilder[] = "[[$str}!~*$v]]"; + break; + // case 'null': + // break; + case '!null': + if ( $label ) { + $searchBuilder[] = "[[$label::+]]"; + } + break; + + } + } + } + } + if ( $datatableData['searchBuilder']['logic'] === 'AND' ) { + $queryConjunction = array_merge( $queryConjunction, $searchBuilder ); + } else if ( $datatableData['searchBuilder']['logic'] === 'OR' ) { + $queryDisjunction = array_merge( $queryDisjunction, $searchBuilder ); + } + } + global $smwgQMaxSize; if ( !count( $queryDisjunction ) ) { $queryDisjunction = ['']; } - $query = $requestParams['query'] . implode( '', $queryConjunction ); + $query = $data['queryString'] . implode( '', $queryConjunction ); $conditions = array_map( static function( $value ) use ( $query ) { return $query . $value; @@ -188,8 +250,6 @@ public function execute() { $queryStr = implode( 'OR', $conditions ); - // trigger_error('queryStr ' . $queryStr); - $log['queryStr '] = $queryStr; $query = SMWQueryProcessor::createQuery( @@ -205,7 +265,6 @@ public function execute() { // $smwgQMaxSize = max( $smwgQMaxSize, $size ); // trigger_error('smwgQMaxSize ' . $smwgQMaxSize); - $applicationFactory = ServicesFactory::getInstance(); $queryEngine = $applicationFactory->getStore(); $results = $queryEngine->getQueryResult( $query ); @@ -234,6 +293,8 @@ public function execute() { 'data' => $res, 'recordsTotal' => $settings['count'], 'recordsFiltered' => $count, + 'cacheKey' => $data['cacheKey'], + 'datalength' => $datatableData['length'] ]; if ( $settings['displayLog'] ) { @@ -275,44 +336,10 @@ public function getHelpUrls() { */ protected function getAllowedParams() { return [ - 'query' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - 'columndefs' => [ + 'data' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, - ], - 'printouts' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - 'printrequests' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - 'settings' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - 'datatable' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - ]; - } - - /** - * Returns an array of parameter descriptions. - * Don't call this function directly: use getFinalParamDescription() to - * allow hooks to modify descriptions as needed. - * - * @return array|bool False on no parameter descriptions - */ - protected function getParamDescription() { - return [ - 'query' => 'Original query', - 'printouts' => 'Printouts used in the original query', + ] ]; } diff --git a/formats/datatables/DataTables.php b/formats/datatables/DataTables.php index 59180d308..17fdafd3e 100644 --- a/formats/datatables/DataTables.php +++ b/formats/datatables/DataTables.php @@ -14,23 +14,15 @@ use Html; use RequestContext; -use SMW\DataValueFactory; -use SMW\DataTypeRegistry; use SMW\ResultPrinter; use SMW\DIWikiPage; -use SMW\DIProperty; -use SMW\Exception\PredefinedPropertyLabelMismatchException; use SMW\Message; -use SMW\SQLStore\SQLStore; -use SMW\SQLStore\TableBuilder\FieldType; -use SMW\QueryFactory; use SMW\Query\PrintRequest; -use SMW\Services\ServicesFactory as ApplicationFactory; -use SMWDataItem as DataItem; +use SMW\Utils\HtmlTable; use SMWPrintRequest; use SMWPropertyValue; -use SMWQueryProcessor; use SMWQueryResult as QueryResult; +use SRF\DataTables\SearchPanes as SearchPanes; class DataTables extends ResultPrinter { @@ -43,29 +35,26 @@ class DataTables extends ResultPrinter { private $printoutsParameters = []; - private $printoutsParametersOptions = []; + public $printoutsParametersOptions = []; private $parser; - /** - * @var boolean - */ + /** @var bool */ private $recursiveAnnotation = false; - private $queryEngineFactory; - - private $store; - - private $query; - - private $connection; + public $store; - private $queryFactory; - - private $searchPanesLog = []; + public $query; + /** @var bool */ private $useAjax; + /** @var HtmlTable */ + private $htmlTable; + + /** @var bool */ + private $hasMultipleValues = false; + /** * @see ResultPrinter::getName * @@ -294,6 +283,73 @@ public function getParamDefinitions( array $definitions ) { 'default' => '', ]; + //////////////// datatables mark + // @see https://markjs.io/#mark + // @see https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/776 + + $params['datatables-mark'] = [ + 'type' => 'boolean', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => false, + ]; + + $params['datatables-mark.separateWordSearch'] = [ + 'type' => 'boolean', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => false, + ]; + + $params['datatables-mark.accuracy'] = [ + 'type' => 'string', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => 'partially', + ]; + + $params['datatables-mark.diacritics'] = [ + 'type' => 'boolean', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => true, + ]; + + $params['datatables-mark.acrossElements'] = [ + 'type' => 'boolean', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => false, + ]; + + $params['datatables-mark.caseSensitive'] = [ + 'type' => 'boolean', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => false, + ]; + + $params['datatables-mark.ignoreJoiners'] = [ + 'type' => 'boolean', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => false, + ]; + + $params['datatables-mark.ignorePunctuation'] = [ + 'type' => 'string', + 'message' => 'srf-paramdesc-datatables-library-option', + // or ':;.,-–—‒_(){}[]!\'"+=' + 'default' => '', + ]; + + $params['datatables-mark.wildcards'] = [ + 'type' => 'string', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => 'disabled', + ]; + + //////////////// datatables searchBuilder + + $params['datatables-searchBuilder'] = [ + 'type' => 'boolean', + 'message' => 'srf-paramdesc-datatables-library-option', + 'default' => false, + ]; + //////////////// datatables searchPanes $params['datatables-searchPanes'] = [ @@ -340,17 +396,6 @@ public function getParamDefinitions( array $definitions ) { 'default' => false, ]; - // ***custom parameter - // use the following as long as searchPanes created server-side - // are used always client-side, for which we need to use - // the trick mentioned below (this isn't, however, strictly - // necessary) - // $params['datatables-searchPanes.forceClient'] = [ - // 'type' => 'boolean', - // 'message' => 'srf-paramdesc-datatables-library-option', - // 'default' => false, - // ]; - // ***custom parameter // @TODO sort panes after rendering using the following // https://github.com/DataTables/SearchPanes/blob/master/src/SearchPane.ts @@ -505,29 +550,49 @@ protected function getResultText( QueryResult $res, $outputmode ) { // @TODO use only one between printouts and printrequests $resultArray = $res->toArray(); - $printrequests = $resultArray['printrequests']; + $printrequests = $resultArray['printrequests']; $result = $this->getResultJson( $res, $outputmode ); + + $this->htmlTable = new HtmlTable(); + foreach ( $headerList as $text ) { + $attributes = []; + $this->htmlTable->header( ( $text === '' ? ' ' : $text ), $attributes ); + } + + foreach ( $result as $i => $rows ) { + $this->htmlTable->row(); + + foreach ( $rows as $cell ) { + $this->htmlTable->cell( + ( $cell === '' ? ' ' : $cell ), + [] + ); + } + if ( $i > $datatablesOptions['pageLength'] ) { + break; + } + } $this->useAjax = $this->query->getOption( 'useAjax' ); - // @TODO use this instead than the block below as long as - // the following trick https://github.com/Knowledge-Wiki/SemanticResultFormats/blob/2230aa3eb8e65dd33ff493ba81269689f50d2945/formats/datatables/resources/ext.srf.formats.datatables.js - // is added to the library - // we use searchPanes server-side also if Ajax isn't required - // since they are more accurate - // $searchpanes = ( !$formattedOptions['searchPanes']['forceClient'] ? - // $this->getSearchPanes( $printRequests, $formattedOptions ) : [] ); - - $searchpanes = ( $this->useAjax ? $this->getSearchPanes( $printRequests, $formattedOptions ) : [] ); + $searchPanesData = []; + $searchPanesLog = []; + if ( array_key_exists( 'searchPanes', $formattedOptions ) + && !empty( $formattedOptions['searchPanes'] ) + && ( $this->useAjax || $this->hasMultipleValues ) ) { + $searchPanes = new SearchPanes( $this ); + $searchPanesData = $searchPanes->getSearchPanes( $printRequests, $formattedOptions['searchPanes'] ); + $searchPanesLog = $searchPanes->getLog(); + } $data = [ 'query' => [ 'ask' => $ask, 'result' => $result ], - 'searchPanes' => $searchpanes, - 'searchPanesLog' => $this->searchPanesLog, + 'searchPanes' => $searchPanesData, + 'searchPanesLog' => $searchPanesLog, 'formattedOptions' => $formattedOptions, 'printoutsParametersOptions' => $this->printoutsParametersOptions ]; @@ -560,8 +625,7 @@ private function printContainer( $data, $headerList, $datatablesOptions, $printr ]); $tableAttrs = [ - 'class' => 'srf-datatable' . ( $this->params['class'] ? ' ' . $this->params['class'] : '' ), - // 'data-theme' => $this->params['theme'], + 'class' => 'srf-datatable wikitable display' . ( $this->params['class'] ? ' ' . $this->params['class'] : '' ), 'data-collation' => !empty( $GLOBALS['smwgEntityCollation'] ) ? $GLOBALS['smwgEntityCollation'] : $GLOBALS['wgCategoryCollation'], 'data-nocase' => ( $GLOBALS['smwgFieldTypeFeatures'] === SMW_FIELDT_CHAR_NOCASE ? true : false ), 'data-column-sort' => json_encode( [ @@ -575,26 +639,44 @@ private function printContainer( $data, $headerList, $datatablesOptions, $printr 'data-use-ajax' => $this->useAjax, 'data-count' => $this->query->getOption( 'count' ), 'data-editor' => $performer->getName(), + 'data-multiple-values' => $this->hasMultipleValues, ]; + + $tableAttrs['width'] = '100%'; + // $tableAttrs['class'] .= ' broadtable'; + + // remove sortable, that triggers jQuery's TableSorter + $classes = preg_split( "/\s+/", $tableAttrs['class'], -1, PREG_SPLIT_NO_EMPTY ); + $key = array_search( 'sortable', $classes ); + if ( $key !== false ) { + unset( $classes[$key] ); + } + $tableAttrs['class'] = implode( " ", $classes ); - // Element includes info, spinner, and container placeholder - return Html::rawElement( - 'div', + $transpose = false; + $html = $this->htmlTable->table( $tableAttrs, - Html::element( - 'div', - [ - 'class' => 'top' - ], - '' - ) . $resourceFormatter->placeholder() . Html::element( - 'div', - [ - 'id' => $id, - 'class' => 'datatables-container', - 'style' => 'display:none;' - ] - ) + $transpose, + $this->isHTML + ); + + // @see https://cdn.datatables.net/v/dt/dt-1.13.8/datatables.js + $datatableSpinner = Html::rawElement( + 'div', + [ + 'class' => 'datatables-spinner dataTables_processing', + 'role' => 'status' + ], + '
' + ); + + return Html::rawElement( + 'div', + [ + 'id' => $id, + 'class' => 'datatables-container', + ], + $datatableSpinner . $html ); } @@ -710,693 +792,6 @@ private function plainToNestedObj( $arr, $value ) { return $ret; } - /** - * @param array $printRequests - * @param array $formattedOptions - * @return array - */ - private function getSearchPanes( $printRequests, $formattedOptions ) { - $searchPanesOptions = $formattedOptions['searchPanes']; - - // searchPanes are disabled - if ( empty( $searchPanesOptions ) ) { - return []; - } - $this->queryEngineFactory = new \SMW\SQLStore\QueryEngineFactory( $this->store ); - $this->connection = $this->store->getConnection( 'mw.db.queryengine' ); - $this->queryFactory = new QueryFactory(); - - $ret = []; - foreach ( $printRequests as $i => $printRequest ) { - if ( count( $searchPanesOptions['columns'] ) && !in_array( $i, $searchPanesOptions['columns'] ) ) { - continue; - } - - $parameterOptions = $this->printoutsParametersOptions[$i]; - - $searchPanesParameterOptions = ( array_key_exists( 'searchPanes', $parameterOptions ) ? - $parameterOptions['searchPanes'] : [] ); - - if ( array_key_exists( 'show', $searchPanesParameterOptions ) && $searchPanesParameterOptions['show'] === false ) { - continue; - } - - $canonicalLabel = ( $printRequest->getMode() !== SMWPrintRequest::PRINT_THIS ? - $printRequest->getCanonicalLabel() : '' ); - - $ret[$i] = $this->getPanesOptions( $printRequest, $canonicalLabel, $searchPanesOptions, $searchPanesParameterOptions ); - } - - return $ret; - } - - /** - * @TODO move to a dedicated class with separate code blocks - * @param PrintRequest $printRequest - * @param string $canonicalLabel - * @param array $searchPanesOptions - * @param array $searchPanesParameterOptions - * @return array - */ - private function getPanesOptions( $printRequest, $canonicalLabel, $searchPanesOptions, $searchPanesParameterOptions ) { - - if ( empty( $canonicalLabel ) ) { - return $this->searchPanesMainlabel( $printRequest, $searchPanesOptions, $searchPanesParameterOptions ); - } - - // create a new query for each printout/pane - // and retrieve the query segment related to it - // then perform the real query to get the results - - $queryParams = [ - 'limit' => $this->query->getLimit(), - 'offset' => $this->query->getOffset(), - 'mainlabel' => $this->query->getMainlabel() - ]; - $queryParams = SMWQueryProcessor::getProcessedParams( $queryParams, [] ); - - // @TODO @FIXME - // get original description and add a conjunction - // $queryDescription = $query->getDescription(); - // $queryCount = new \SMWQuery($queryDescription); - // ... - - $isCategory = $printRequest->getMode() === PrintRequest::PRINT_CATS; - - // @TODO @FIXME cover PRINT_CHAIN as well - $newQuery = SMWQueryProcessor::createQuery( - $this->query->getQueryString() . ( !$isCategory ? '[[' . $canonicalLabel . '::+]]' : '' ), - $queryParams, - SMWQueryProcessor::INLINE_QUERY, - '' - ); - - $queryDescription = $newQuery->getDescription(); - $queryDescription->setPrintRequests( [$printRequest] ); - - $conditionBuilder = $this->queryEngineFactory->newConditionBuilder(); - - $rootid = $conditionBuilder->buildCondition( $newQuery ); - - \SMW\SQLStore\QueryEngine\QuerySegment::$qnum = 0; - $querySegmentList = $conditionBuilder->getQuerySegmentList(); - - $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor(); - - $querySegmentListProcessor->setQuerySegmentList( $querySegmentList ); - - // execute query tree, resolve all dependencies - $querySegmentListProcessor->process( $rootid ); - - $qobj = $querySegmentList[$rootid]; - - $property = new DIProperty( DIProperty::newFromUserLabel( $printRequest->getCanonicalLabel() ) ); - - if ( $isCategory ) { - - // data-length without the GROUP BY clause - $sql_options = [ 'LIMIT' => 1 ]; - - $dataLength = (int)$this->connection->selectField( - $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from - . ' JOIN ' . $this->connection->tableName( 'smw_fpt_inst' ) . " AS insts ON $qobj->alias.smw_id = insts.s_id", - "COUNT(*) AS count", - $qobj->where, - __METHOD__, - $sql_options - ); - - if ( !$dataLength ) { - return []; - } - - $groupBy = "i.smw_id"; - $orderBy = "count DESC, $groupBy ASC"; - $sql_options = [ - 'GROUP BY' => $groupBy, - 'LIMIT' => $dataLength, // $this->query->getOption( 'count' ), - 'ORDER BY' => $orderBy, - 'HAVING' => 'count >= ' . $searchPanesOptions['minCount'] - ]; - - /* - SELECT COUNT(i.smw_id), i.smw_id, i.smw_title FROM `smw_object_ids` AS t0 - JOIN `smw_fpt_inst` AS t1 ON t0.smw_id=t1.s_id - JOIN `smw_fpt_inst` AS insts ON t0.smw_id=insts.s_id - JOIN `smw_object_ids` AS i ON i.smw_id = insts.o_id - where (t1.o_id=1077) - GROUP BY i.smw_id - HAVING COUNT(i.smw_id) >= 1 ORDER BY COUNT(i.smw_id) DESC - */ - - $res = $this->connection->select( - $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from - // @see https://github.com/SemanticMediaWiki/SemanticDrilldown/blob/master/includes/Sql/SqlProvider.php - . ' JOIN ' . $this->connection->tableName( 'smw_fpt_inst' ) . " AS insts ON $qobj->alias.smw_id = insts.s_id" - . ' JOIN ' . $this->connection->tableName( SQLStore::ID_TABLE ) . " AS i ON i.smw_id = insts.o_id", - "COUNT($groupBy) AS count, i.smw_id, i.smw_title, i.smw_namespace, i.smw_iw, i.smw_sort, i.smw_subobject", - $qobj->where, - __METHOD__, - $sql_options - ); - - $isIdField = true; - - } else { - - $tableid = $this->store->findPropertyTableID( $property ); - - $querySegmentList = array_reverse( $querySegmentList ); - - // get aliases - $p_alias = null; - foreach ( $querySegmentList as $segment ) { - if ( $segment->joinTable === $tableid ) { - $p_alias = $segment->alias; - break; - } - } - - if ( empty( $p_alias ) ) { - $this->searchPanesLog[] = [ - 'canonicalLabel' => $printRequest->getCanonicalLabel(), - 'error' => '$p_alias is null', - ]; - return []; - } - - // data-length without the GROUP BY clause - $sql_options = [ 'LIMIT' => 1 ]; - - // SELECT COUNT(*) as count FROM `smw_object_ids` AS t0 - // INNER JOIN (`smw_fpt_mdat` AS t2 INNER JOIN `smw_di_wikipage` AS t3 ON t2.s_id=t3.s_id) ON t0.smw_id=t2.s_id - // WHERE ((t3.p_id=517)) LIMIT 500 - - $dataLength = (int)$this->connection->selectField( - $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from, - "COUNT(*) as count", - $qobj->where, - __METHOD__, - $sql_options - ); - - if ( !$dataLength ) { - return []; - } - - list( $diType, $isIdField, $fields, $groupBy, $orderBy ) = $this->fetchValuesByGroup( $property, $p_alias ); - - /* - ---GENERATED DATATABLES - - SELECT t0.smw_id,t0.smw_title,t0.smw_namespace,t0.smw_iw,t0.smw_subobject,t0.smw_hash,t0.smw_sort,COUNT( t3.o_id ) as count FROM `smw_object_ids` AS t0 INNER JOIN (`smw_fpt_mdat` AS t2 INNER JOIN `smw_di_wikipage` AS t3 ON t2.s_id=t3.s_id <> ) ON t0.smw_id=t2.s_id WHERE ((t3.p_id=517)) GROUP BY t3.o_id, t0.smw_id HAVING count >= 1 ORDER BY count DESC, t0.smw_sort ASC LIMIT 500 - - ---GENERATED ByGroupPropertyValuesLookup - SELECT i.smw_id,i.smw_title,i.smw_namespace,i.smw_iw,i.smw_subobject,i.smw_hash,i.smw_sort,COUNT( p.o_id ) as count FROM `smw_object_ids` `o` INNER JOIN `smw_di_wikipage` `p` ON ((p.s_id=o.smw_id)) JOIN `smw_object_ids` `i` ON ((p.o_id=i.smw_id)) WHERE o.smw_hash IN ('1_-_A','1_-_Ab','1_-_Abc','10_-_Abcd','11_-_Abc') AND (o.smw_iw!=':smw') AND (o.smw_iw!=':smw-delete') AND p.p_id = 517 GROUP BY p.o_id, i.smw_id ORDER BY count DESC, i.smw_sort ASC - - */ - - global $smwgQMaxLimit; - - $sql_options = [ - 'GROUP BY' => $groupBy, - // the following implies that if the user sets a threshold - // close or equal to 1, and there are too many unique values, - // the page will break, however the user has responsibility - // for using searchPanes only for data reasonably grouped - // shouldn't be 'LIMIT' => $smwgQMaxLimit, ? - 'LIMIT' => $dataLength, - 'ORDER BY' => $orderBy, - 'HAVING' => 'count >= ' . $searchPanesOptions['minCount'] - ]; - - // @see QueryEngine - $res = $this->connection->select( - $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from - . ( !$isIdField ? '' - : " JOIN " . $this->connection->tableName( SQLStore::ID_TABLE ) . " AS `i` ON ($p_alias.o_id = i.smw_id)" ), - implode( ',', $fields ), - $qobj->where . ( !$isIdField ? '' : ( !empty( $qobj->where ) ? ' AND' : '' ) - . ' i.smw_iw!=' . $this->connection->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) - . ' AND i.smw_iw!=' . $this->connection->addQuotes( SMW_SQL3_SMWDELETEIW ) ), - __METHOD__, - $sql_options - ); - - } - - // verify uniqueRatio - - // @see https://datatables.net/extensions/searchpanes/examples/initialisation/threshold.htm - // @see https://github.com/DataTables/SearchPanes/blob/818900b75dba6238bf4b62a204fdd41a9b8944b7/src/SearchPane.ts#L824 - - $threshold = !empty( $searchPanesParameterOptions['threshold'] ) ? - $searchPanesParameterOptions['threshold'] : $searchPanesOptions['threshold']; - - $outputFormat = $printRequest->getOutputFormat(); - - // *** if outputFormat is not set we can compute - // uniqueness ratio by now, otherwise we have to - // perform it after grouping the actual data - if ( !$outputFormat ) { - $binLength = $res->numRows(); - $uniqueRatio = $binLength / $dataLength; - - $this->searchPanesLog[] = [ - 'canonicalLabel' => $printRequest->getCanonicalLabel(), - 'dataLength' => $dataLength, - 'binLength' => $binLength, - 'uniqueRatio' => $uniqueRatio, - 'threshold' => $threshold, - 'grouped' => false, - ]; - - // || $binLength <= 1 - if ( $uniqueRatio > $threshold ) { - return []; - } - } - - // @see ByGroupPropertyValuesLookup - $diType = DataTypeRegistry::getInstance()->getDataItemId( - $property->findPropertyTypeID() - ); - - $diHandler = $this->store->getDataItemHandlerForDIType( - $diType - ); - - $fields = $diHandler->getFetchFields(); - - $deepRedirectTargetResolver = ApplicationFactory::getInstance() - ->newMwCollaboratorFactory()->newDeepRedirectTargetResolver(); - - $outputMode = SMW_OUTPUT_HTML; - $isSubject = false; - $groups = []; - foreach ( $res as $row ) { - - if ( $isIdField ) { - $dbKeys = [ - $row->smw_title, - $row->smw_namespace, - $row->smw_iw, - $row->smw_sort, - $row->smw_subobject - ]; - - } else { - $dbKeys = []; - foreach ( $fields as $field => $fieldType ) { - $dbKeys[] = $row->$field; - } - } - - $dbKeys = count( $dbKeys ) > 1 ? $dbKeys : $dbKeys[0]; - - $dataItem = $diHandler->dataItemFromDBKeys( - $dbKeys - ); - - // try to resolve redirect - if ( $isIdField && $row->smw_iw === SMW_SQL3_SMWREDIIW ) { - $redirectTarget = null; - // @see SMWExportController - try { - $redirectTarget = $deepRedirectTargetResolver->findRedirectTargetFor( $dataItem->getTitle() ); - } catch ( \Exception $e ) { - } - if ( $redirectTarget ) { - $dataItem = DIWikiPage::newFromTitle( $redirectTarget ); - } - } - - $dataValue = DataValueFactory::getInstance()->newDataValueByItem( - $dataItem, - $property - ); - - if ( $outputFormat ) { - $dataValue->setOutputFormat( $outputFormat ); - } - - $cellContent = $this->getCellContent( - $printRequest->getCanonicalLabel(), - [ $dataValue ], - $outputMode, - $isSubject - ); - - if ( !array_key_exists( $cellContent, $groups ) ) { - $groups[$cellContent] = [ 'count' => 0, 'value' => '' ]; - - if ( $dataItem->getDiType() === DataItem::TYPE_TIME ) { - // max Unix time - $groups[$cellContent]['minDate'] = 2147483647; - $groups[$cellContent]['maxDate'] = 0; - } - } - - $groups[$cellContent]['count'] += $row->count; - - // @TODO complete with all the possible transformations of - // datavalues (DataValues/ValueFormatters) - // based on $printRequest->getOutputFormat() - // and provide to the API the information to - // rebuild the query when values are grouped - // by the output of the printout format, e.g. - // if grouped by unit (for number datatype) - // value should be *, for datetime see the - // method below - - switch( $dataItem->getDiType() ) { - case DataItem::TYPE_NUMBER: - if ( $outputFormat === '-u' ) { - $value = '*'; - } else { - $value = $dataValue->getNumber(); - } - break; - - case DataItem::TYPE_BLOB: - // @see IntlNumberFormatter - // $requestedLength = intval( $outputFormat ); - $value = $dataValue->getWikiValue(); - break; - - case DataItem::TYPE_BOOLEAN: - $value = $dataValue->getWikiValue(); - break; - - case DataItem::TYPE_URI: - $value = $dataValue->getWikiValue(); - break; - - case DataItem::TYPE_TIME: - $currentDate = $dataItem->asDateTime()->getTimestamp(); - $value = $dataValue->getISO8601Date(); - if ( $currentDate < $groups[$cellContent]['minDate'] ) { - $groups[$cellContent]['minDate'] = $currentDate; - } - if ( $currentDate > $groups[$cellContent]['maxDate'] ) { - $groups[$cellContent]['maxDate'] = $currentDate; - } - break; - - case DataItem::TYPE_GEO: - $value = $dataValue->getWikiValue(); - break; - - case DataItem::TYPE_CONTAINER: - $value = $dataValue->getWikiValue(); - break; - - case DataItem::TYPE_WIKIPAGE: - $title_ = $dataValue->getTitle(); - if ( $title_ ) { - $value = $title_->getFullText(); - } else { - $value = $dataValue->getWikiValue(); - $this->searchPanesLog[] = [ - 'canonicalLabel' => $printRequest->getCanonicalLabel(), - 'error' => 'TYPE_WIKIPAGE title is null', - 'wikiValue' => $value, - ]; - } - break; - - case DataItem::TYPE_CONCEPT: - $value = $dataValue->getWikiValue(); - break; - - case DataItem::TYPE_PROPERTY: - - break; - case DataItem::TYPE_NOTYPE: - $value = $dataValue->getWikiValue(); - break; - - default: - $value = $dataValue->getWikiValue(); - - } - - $groups[$cellContent]['value'] = $value; - } - - if ( $outputFormat ) { - $binLength = count( $groups ); - $uniqueRatio = $binLength / $dataLength; - - $this->searchPanesLog[] = [ - 'canonicalLabel' => $printRequest->getCanonicalLabel(), - 'dataLength' => $dataLength, - 'binLength' => $binLength, - 'uniqueRatio' => $uniqueRatio, - 'threshold' => $threshold, - 'grouped' => true, - ]; - - // || $binLength <= 1 - if ( $uniqueRatio > $threshold ) { - return []; - } - - } - - arsort( $groups, SORT_NUMERIC ); - - $ret = []; - foreach( $groups as $content => $value ) { - - // @see https://www.semantic-mediawiki.org/wiki/Help:Search_operators - // the latest value is returned, with the largest range - if ( array_key_exists( 'minDate', $value ) && $value['minDate'] != $value['maxDate'] ) { - // ISO 8601 - // @TODO use a symbol instead and transform from the API - $value['value'] = '>' . date( 'c', $value['minDate'] ) . ']][[' . $printRequest->getCanonicalLabel() . '::<' . date( 'c', $value['maxDate'] ); - } - - $ret[] = [ - 'label' => $content, - 'count' => $value['count'], - 'value' => $value['value'] - ]; - } - - return $ret; - } - - /** - * @see ByGroupPropertyValuesLookup - * @param DIProperty $property - * @param string $p_alias - * @return array - */ - private function fetchValuesByGroup( DIProperty $property, $p_alias ) { - - $tableid = $this->store->findPropertyTableID( $property ); - // $entityIdManager = $this->store->getObjectIds(); - - $proptables = $this->store->getPropertyTables(); - - // || $subjects === [] - if ( $tableid === '' || !isset( $proptables[$tableid] ) ) { - return []; - } - - $connection = $this->store->getConnection( 'mw.db' ); - - $propTable = $proptables[$tableid]; - $isIdField = false; - - $diHandler = $this->store->getDataItemHandlerForDIType( - $propTable->getDiType() - ); - - foreach ( $diHandler->getFetchFields() as $field => $fieldType ) { - if ( !$isIdField && $fieldType === FieldType::FIELD_ID ) { - $isIdField = true; - } - } - - $groupBy = $diHandler->getLabelField(); - $pid = ''; - - if ( $groupBy === '' ) { - $groupBy = $diHandler->getIndexField(); - } - - $groupBy = "$p_alias.$groupBy"; - $orderBy = "count DESC, $groupBy ASC"; - - $diType = $propTable->getDiType(); - - if ( $diType === DataItem::TYPE_WIKIPAGE ) { - $fields = [ - "i.smw_id", - "i.smw_title", - "i.smw_namespace", - "i.smw_iw", - "i.smw_subobject", - "i.smw_hash", - "i.smw_sort", - "COUNT( $groupBy ) as count" - ]; - - $groupBy = "$p_alias.o_id, i.smw_id"; - $orderBy = "count DESC, i.smw_sort ASC"; - } elseif ( $diType === DataItem::TYPE_BLOB ) { - $fields = [ "$p_alias.o_hash, $p_alias.o_blob", "COUNT( $p_alias.o_hash ) as count" ]; - $groupBy = "$p_alias.o_hash, $p_alias.o_blob"; - } elseif ( $diType === DataItem::TYPE_URI ) { - $fields = [ "$p_alias.o_serialized, $p_alias.o_blob", "COUNT( $p_alias.o_serialized ) as count" ]; - $groupBy = "$p_alias.o_serialized, $p_alias.o_blob"; - } elseif ( $diType === DataItem::TYPE_NUMBER ) { - $fields = [ "$p_alias.o_serialized,$p_alias.o_sortkey, COUNT( $p_alias.o_serialized ) as count" ]; - $groupBy = "$p_alias.o_serialized,$p_alias.o_sortkey"; - $orderBy = "count DESC, $p_alias.o_sortkey DESC"; - } else { - $fields = [ "$groupBy", "COUNT( $groupBy ) as count" ]; - } - - // if ( !$propTable->isFixedPropertyTable() ) { - // $pid = $entityIdManager->getSMWPropertyID( $property ); - // } - - return [ $diType, $isIdField, $fields, $groupBy, $orderBy ]; - } - - /** - * @param PrintRequest $printRequest - * @param array $searchPanesOptions - * @param array $searchPanesParameterOptions - * @return array - */ - private function searchPanesMainlabel( $printRequest, $searchPanesOptions, $searchPanesParameterOptions ) { - - // mainlabel consists only of unique values, - // so do not display if settings don't allow that - if ( $searchPanesOptions['minCount'] > 1 ) { - return []; - } - - $threshold = !empty( $searchPanesParameterOptions['threshold'] ) ? - $searchPanesParameterOptions['threshold'] : $searchPanesOptions['threshold']; - - $this->searchPanesLog[] = [ - 'canonicalLabel' => 'mainLabel', - 'threshold' => $threshold, - ]; - - if ( $threshold < 1 ) { - return []; - } - - $query = $this->query; - $queryDescription = $query->getDescription(); - $queryDescription->setPrintRequests( [] ); - - $conditionBuilder = $this->queryEngineFactory->newConditionBuilder(); - $rootid = $conditionBuilder->buildCondition( $query ); - - \SMW\SQLStore\QueryEngine\QuerySegment::$qnum = 0; - $querySegmentList = $conditionBuilder->getQuerySegmentList(); - - $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor(); - - $querySegmentListProcessor->setQuerySegmentList( $querySegmentList ); - - // execute query tree, resolve all dependencies - $querySegmentListProcessor->process( $rootid ); - - $qobj = $querySegmentList[$rootid]; - - global $smwgQMaxLimit; - - $sql_options = [ - // *** should we set a limit here ? - // it makes sense to show the pane for - // mainlabel only when page titles are grouped - // through the printout format or even the printout template - // 'LIMIT' => $smwgQMaxLimit, - // title - 'ORDER BY' => 't' - ]; - - // Selecting those is required in standard SQL (but MySQL does not require it). - $sortfields = implode( ',', $qobj->sortfields ); - $sortfields = $sortfields ? ',' . $sortfields : ''; - - // @see QueryEngine - $res = $this->connection->select( - $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from, - "$qobj->alias.smw_id AS id," . - "$qobj->alias.smw_title AS t," . - "$qobj->alias.smw_namespace AS ns," . - "$qobj->alias.smw_iw AS iw," . - "$qobj->alias.smw_subobject AS so," . - "$qobj->alias.smw_sortkey AS sortkey" . - "$sortfields", - $qobj->where, - __METHOD__, - $sql_options - ); - - $diHandler = $this->store->getDataItemHandlerForDIType( - DataItem::TYPE_WIKIPAGE - ); - - $outputMode = SMW_OUTPUT_HTML; - $isSubject = false; - $groups = []; - foreach( $res as $row) { - - $dataItem = $diHandler->dataItemFromDBKeys( [ - $row->t, - intval( $row->ns ), - $row->iw, - '', - $row->so - ] ); - - $dataValue = DataValueFactory::getInstance()->newDataValueByItem( - $dataItem - ); - - if ( $printRequest->getOutputFormat() ) { - $dataValue->setOutputFormat( $printRequest->getOutputFormat() ); - } - - $cellContent = $this->getCellContent( - $printRequest->getCanonicalLabel(), - [ $dataValue ], - $outputMode, - $isSubject - ); - - if ( !array_key_exists( $cellContent, $groups ) ) { - $groups[$cellContent] = [ 'count' => 0, 'value' => '' ]; - } - - $groups[$cellContent]['count']++; - $groups[$cellContent]['value'] = $dataValue->getTitle()->getText(); - } - - arsort( $groups, SORT_NUMERIC ); - - $ret = []; - foreach( $groups as $content => $value ) { - $ret[] = [ - 'label' => $content, - 'value' => $value['value'], - 'count' => $value['count'] - ]; - } - - return $ret; - } - /** * @param array $params * @return array @@ -1406,6 +801,7 @@ private function formatOptions( $params ) { 'lengthMenu' => "number", 'buttons' => "string", 'searchPanes.columns' => "number", + 'mark.ignorePunctuation' => "number", // ... ]; @@ -1471,7 +867,6 @@ public function getResultJson( QueryResult $res, $outputMode ) { $ret = []; while ( $subject = $res->getNext() ) { - $row = []; foreach ( $subject as $i => $field ) { $dataValues = []; @@ -1503,8 +898,17 @@ public function getResultJson( QueryResult $res, $outputMode ) { return $ret; } - // @see SMW\Query\ResultPrinters\TableResultPrinter - protected function getCellContent( string $label, array $dataValues, $outputMode, $isSubject ) { + /** + * @see SMW\Query\ResultPrinters\TableResultPrinter + * @param string $label + * @param array $dataValues + * @param int $outputMode + * @param bool $isSubject + * @param string $propTypeid + * @return array + */ + public function getCellContent( $label, $dataValues, $outputMode, $isSubject, $propTypeid = null ) { + if ( !$this->prefixParameterProcessor ) { $dataValueMethod = 'getShortText'; } else { @@ -1523,13 +927,16 @@ protected function getCellContent( string $label, array $dataValues, $outputMode $outputMode = SMW_OUTPUT_WIKI; } + // this is only used by SearchPanes + $isKeyword = ( $propTypeid === '_keyw' ); $values = []; foreach ( $dataValues as $dv ) { + $dataItem = $dv->getDataItem(); // Restore output in Special:Ask on: // - file/image parsing // - text formatting on string elements including italic, bold etc. - if ( $outputMode === SMW_OUTPUT_HTML && $dv->getDataItem() instanceof DIWikiPage && $dv->getDataItem()->getNamespace() === NS_FILE || - $outputMode === SMW_OUTPUT_HTML && $dv->getDataItem() instanceof DIBlob ) { + if ( $outputMode === SMW_OUTPUT_HTML && $dataItem instanceof DIWikiPage && $dataItem->getNamespace() === NS_FILE || + $outputMode === SMW_OUTPUT_HTML && $dataItem instanceof DIBlob ) { // Too lazy to handle the Parser object and besides the Message // parse does the job and ensures no other hook is executed $value = Message::get( @@ -1540,14 +947,30 @@ protected function getCellContent( string $label, array $dataValues, $outputMode $value = $dv->$dataValueMethod( $outputMode, $this->getLinker( $isSubject ) ); } + // @FIXME this is not the best way, + // try to use $isKeyword = $dataItem->getOption( 'is.keyword' ); + // @see DIBlobHandler + if ( $isKeyword ) { + $value = $dataItem->normalize( $value ); + } + if ( $template ) { - $value = $this->parser->recursiveTagParseFully( '{{' . $template . '|' . $value . '}}' ); + // escape pipe character + $value_ = str_replace( '|', '|', (string)$value ); + $value = $this->parser->recursiveTagParseFully( '{{' . $template . '|' . $value_ . '}}' ); } $values[] = $value === '' ? ' ' : $value; } $sep = strtolower( $this->params['sep'] ); + + // *** used to force use of Ajax with + // searchpanes since a client side solution + // won't produce reliable matches + if ( count( $values ) > 1 ) { + $this->hasMultipleValues = true; + } if ( !$isSubject && $sep === 'ul' && count( $values ) > 1 ) { $html = '
  • ' . implode( '
  • ', $values ) . '
'; diff --git a/formats/datatables/SearchPanes.php b/formats/datatables/SearchPanes.php new file mode 100644 index 000000000..4394111ad --- /dev/null +++ b/formats/datatables/SearchPanes.php @@ -0,0 +1,746 @@ +datatables = $datatables; + } + + /** + * @param array $printRequests + * @param array $searchPanesOptions + * @return array + */ + public function getSearchPanes( $printRequests, $searchPanesOptions ) { + $this->queryEngineFactory = new QueryEngineFactory( $this->datatables->store ); + $this->connection = $this->datatables->store->getConnection( 'mw.db.queryengine' ); + $this->queryFactory = new QueryFactory(); + + $ret = []; + foreach ( $printRequests as $i => $printRequest ) { + if ( count( $searchPanesOptions['columns'] ) && !in_array( $i, $searchPanesOptions['columns'] ) ) { + continue; + } + + $parameterOptions = $this->datatables->printoutsParametersOptions[$i]; + + $searchPanesParameterOptions = ( array_key_exists( 'searchPanes', $parameterOptions ) ? + $parameterOptions['searchPanes'] : [] ); + + if ( array_key_exists( 'show', $searchPanesParameterOptions ) && $searchPanesParameterOptions['show'] === false ) { + continue; + } + + $canonicalLabel = ( $printRequest->getMode() !== SMWPrintRequest::PRINT_THIS ? + $printRequest->getCanonicalLabel() : '' ); + + $ret[$i] = $this->getPanesOptions( $printRequest, $canonicalLabel, $searchPanesOptions, $searchPanesParameterOptions ); + } + + return $ret; + } + + /** + * @return array + */ + public function getLog() { + return $this->searchPanesLog; + } + + /** + * @param PrintRequest $printRequest + * @param string $canonicalLabel + * @param array $searchPanesOptions + * @param array $searchPanesParameterOptions + * @return array + */ + private function getPanesOptions( $printRequest, $canonicalLabel, $searchPanesOptions, $searchPanesParameterOptions ) { + + if ( empty( $canonicalLabel ) ) { + return $this->searchPanesMainlabel( $printRequest, $searchPanesOptions, $searchPanesParameterOptions ); + } + + // create a new query for each printout/pane + // and retrieve the query segment related to it + // then perform the real query to get the results + + $queryParams = [ + 'limit' => $this->datatables->query->getLimit(), + 'offset' => $this->datatables->query->getOffset(), + 'mainlabel' => $this->datatables->query->getMainlabel() + ]; + $queryParams = SMWQueryProcessor::getProcessedParams( $queryParams, [] ); + + // @TODO @FIXME + // get original description and add a conjunction + // $queryDescription = $query->getDescription(); + // $queryCount = new \SMWQuery($queryDescription); + // ... + + $isCategory = $printRequest->getMode() === PrintRequest::PRINT_CATS; + + // @TODO @FIXME cover PRINT_CHAIN as well + $newQuery = SMWQueryProcessor::createQuery( + $this->datatables->query->getQueryString() . ( !$isCategory ? '[[' . $canonicalLabel . '::+]]' : '' ), + $queryParams, + SMWQueryProcessor::INLINE_QUERY, + '' + ); + + $queryDescription = $newQuery->getDescription(); + $queryDescription->setPrintRequests( [$printRequest] ); + + $conditionBuilder = $this->queryEngineFactory->newConditionBuilder(); + + $rootid = $conditionBuilder->buildCondition( $newQuery ); + + \SMW\SQLStore\QueryEngine\QuerySegment::$qnum = 0; + $querySegmentList = $conditionBuilder->getQuerySegmentList(); + + $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor(); + + $querySegmentListProcessor->setQuerySegmentList( $querySegmentList ); + + // execute query tree, resolve all dependencies + $querySegmentListProcessor->process( $rootid ); + + $qobj = $querySegmentList[$rootid]; + + $property = new DIProperty( DIProperty::newFromUserLabel( $printRequest->getCanonicalLabel() ) ); + $propTypeid = $property->findPropertyTypeID(); + + if ( $isCategory ) { + + // data-length without the GROUP BY clause + $sql_options = [ 'LIMIT' => 1 ]; + + $dataLength = (int)$this->connection->selectField( + $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from + . ' JOIN ' . $this->connection->tableName( 'smw_fpt_inst' ) . " AS insts ON $qobj->alias.smw_id = insts.s_id", + "COUNT(*) AS count", + $qobj->where, + __METHOD__, + $sql_options + ); + + if ( !$dataLength ) { + return []; + } + + $groupBy = "i.smw_id"; + $orderBy = "count DESC, $groupBy ASC"; + $sql_options = [ + 'GROUP BY' => $groupBy, + 'LIMIT' => $dataLength, // $this->query->getOption( 'count' ), + 'ORDER BY' => $orderBy, + 'HAVING' => 'count >= ' . $searchPanesOptions['minCount'] + ]; + + /* + SELECT COUNT(i.smw_id), i.smw_id, i.smw_title FROM `smw_object_ids` AS t0 + JOIN `smw_fpt_inst` AS t1 ON t0.smw_id=t1.s_id + JOIN `smw_fpt_inst` AS insts ON t0.smw_id=insts.s_id + JOIN `smw_object_ids` AS i ON i.smw_id = insts.o_id + WHERE (t1.o_id=1077) + GROUP BY i.smw_id + HAVING COUNT(i.smw_id) >= 1 ORDER BY COUNT(i.smw_id) DESC + */ + + $res = $this->connection->select( + $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from + // @see https://github.com/SemanticMediaWiki/SemanticDrilldown/blob/master/includes/Sql/SqlProvider.php + . ' JOIN ' . $this->connection->tableName( 'smw_fpt_inst' ) . " AS insts ON $qobj->alias.smw_id = insts.s_id" + . ' JOIN ' . $this->connection->tableName( SQLStore::ID_TABLE ) . " AS i ON i.smw_id = insts.o_id", + "COUNT($groupBy) AS count, i.smw_id, i.smw_title, i.smw_namespace, i.smw_iw, i.smw_sort, i.smw_subobject", + $qobj->where, + __METHOD__, + $sql_options + ); + + $isIdField = true; + + } else { + $tableid = $this->datatables->store->findPropertyTableID( $property ); + + $querySegmentList = array_reverse( $querySegmentList ); + + // get aliases + $p_alias = null; + foreach ( $querySegmentList as $segment ) { + if ( $segment->joinTable === $tableid ) { + $p_alias = $segment->alias; + break; + } + } + + if ( empty( $p_alias ) ) { + $this->searchPanesLog[] = [ + 'canonicalLabel' => $printRequest->getCanonicalLabel(), + 'error' => '$p_alias is null', + ]; + return []; + } + + // data-length without the GROUP BY clause + $sql_options = [ 'LIMIT' => 1 ]; + + // SELECT COUNT(*) as count FROM `smw_object_ids` AS t0 + // INNER JOIN (`smw_fpt_mdat` AS t2 INNER JOIN `smw_di_wikipage` AS t3 ON t2.s_id=t3.s_id) ON t0.smw_id=t2.s_id + // WHERE ((t3.p_id=517)) LIMIT 500 + + $dataLength = (int)$this->connection->selectField( + $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from, + "COUNT(*) as count", + $qobj->where, + __METHOD__, + $sql_options + ); + + if ( !$dataLength ) { + return []; + } + + list( $diType, $isIdField, $fields, $groupBy, $orderBy ) = $this->fetchValuesByGroup( $property, $p_alias, $propTypeid ); + + /* + ---GENERATED FROM DATATABLES + SELECT t0.smw_id,t0.smw_title,t0.smw_namespace,t0.smw_iw,t0.smw_subobject,t0.smw_hash,t0.smw_sort,COUNT( t3.o_id ) as count FROM `smw_object_ids` AS t0 INNER JOIN (`smw_fpt_mdat` AS t2 INNER JOIN `smw_di_wikipage` AS t3 ON t2.s_id=t3.s_id <> ) ON t0.smw_id=t2.s_id WHERE ((t3.p_id=517)) GROUP BY t3.o_id, t0.smw_id HAVING count >= 1 ORDER BY count DESC, t0.smw_sort ASC LIMIT 500 + + ---GENERATED ByGroupPropertyValuesLookup + SELECT i.smw_id,i.smw_title,i.smw_namespace,i.smw_iw,i.smw_subobject,i.smw_hash,i.smw_sort,COUNT( p.o_id ) as count FROM `smw_object_ids` `o` INNER JOIN `smw_di_wikipage` `p` ON ((p.s_id=o.smw_id)) JOIN `smw_object_ids` `i` ON ((p.o_id=i.smw_id)) WHERE o.smw_hash IN ('1_-_A','1_-_Ab','1_-_Abc','10_-_Abcd','11_-_Abc') AND (o.smw_iw!=':smw') AND (o.smw_iw!=':smw-delete') AND p.p_id = 517 GROUP BY p.o_id, i.smw_id ORDER BY count DESC, i.smw_sort ASC + */ + + global $smwgQMaxLimit; + + $sql_options = [ + 'GROUP BY' => $groupBy, + // the following implies that if the user sets a threshold + // close or equal to 1, and there are too many unique values, + // the page will break, however the user has responsibility + // for using searchPanes only for data reasonably grouped + // shouldn't be 'LIMIT' => $smwgQMaxLimit, ? + 'LIMIT' => $dataLength, + 'ORDER BY' => $orderBy, + 'HAVING' => 'count >= ' . $searchPanesOptions['minCount'] + ]; + + // @see QueryEngine + $res = $this->connection->select( + $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from + . ( !$isIdField ? '' + : " JOIN " . $this->connection->tableName( SQLStore::ID_TABLE ) . " AS `i` ON ($p_alias.o_id = i.smw_id)" ), + implode( ',', $fields ), + $qobj->where . ( !$isIdField ? '' : ( !empty( $qobj->where ) ? ' AND' : '' ) + . ' i.smw_iw!=' . $this->connection->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) + . ' AND i.smw_iw!=' . $this->connection->addQuotes( SMW_SQL3_SMWDELETEIW ) ), + __METHOD__, + $sql_options + ); + + } + + // verify uniqueRatio + + // @see https://datatables.net/extensions/searchpanes/examples/initialisation/threshold.htm + // @see https://github.com/DataTables/SearchPanes/blob/818900b75dba6238bf4b62a204fdd41a9b8944b7/src/SearchPane.ts#L824 + + $threshold = !empty( $searchPanesParameterOptions['threshold'] ) ? + $searchPanesParameterOptions['threshold'] : $searchPanesOptions['threshold']; + + $outputFormat = $printRequest->getOutputFormat(); + + // *** if outputFormat is not set we can compute + // uniqueness ratio by now, otherwise we have to + // perform it after grouping the actual data + if ( !$outputFormat ) { + $binLength = $res->numRows(); + $uniqueRatio = $binLength / $dataLength; + + $this->searchPanesLog[] = [ + 'canonicalLabel' => $printRequest->getCanonicalLabel(), + 'dataLength' => $dataLength, + 'binLength' => $binLength, + 'uniqueRatio' => $uniqueRatio, + 'threshold' => $threshold, + 'grouped' => false, + ]; + + // || $binLength <= 1 + if ( $uniqueRatio > $threshold ) { + return []; + } + } + + // @see ByGroupPropertyValuesLookup + $diType = DataTypeRegistry::getInstance()->getDataItemId( + $propTypeid + ); + + $diHandler = $this->datatables->store->getDataItemHandlerForDIType( + $diType + ); + + $fields = $diHandler->getFetchFields(); + + $deepRedirectTargetResolver = ApplicationFactory::getInstance() + ->newMwCollaboratorFactory()->newDeepRedirectTargetResolver(); + + $outputMode = SMW_OUTPUT_HTML; + $isSubject = false; + $groups = []; + foreach ( $res as $row ) { + + if ( $isIdField ) { + $dbKeys = [ + $row->smw_title, + $row->smw_namespace, + $row->smw_iw, + $row->smw_sort, + $row->smw_subobject + ]; + + } else { + $dbKeys = []; + foreach ( $fields as $field => $fieldType ) { + $dbKeys[] = $row->$field; + } + } + + $dbKeys = count( $dbKeys ) > 1 ? $dbKeys : $dbKeys[0]; + + $dataItem = $diHandler->dataItemFromDBKeys( + $dbKeys + ); + + // try to resolve redirect + if ( $isIdField && $row->smw_iw === SMW_SQL3_SMWREDIIW ) { + $redirectTarget = null; + // @see SMWExportController + try { + $redirectTarget = $deepRedirectTargetResolver->findRedirectTargetFor( $dataItem->getTitle() ); + } catch ( \Exception $e ) { + } + if ( $redirectTarget ) { + $dataItem = DIWikiPage::newFromTitle( $redirectTarget ); + } + } + + $dataValue = DataValueFactory::getInstance()->newDataValueByItem( + $dataItem, + $property + ); + + if ( $outputFormat ) { + $dataValue->setOutputFormat( $outputFormat ); + } + + +/* + + + // @see DIBlobHandler + // $isKeyword = $dataItem->getOption( 'is.keyword' ); + + if ( $propTypeid === '_keyw' ) { + $value = $dataItem->normalize( $value ); + } + + */ + $cellContent = $this->datatables->getCellContent( + $printRequest->getCanonicalLabel(), + [ $dataValue ], + $outputMode, + $isSubject, + $propTypeid + ); + + if ( !array_key_exists( $cellContent, $groups ) ) { + $groups[$cellContent] = [ 'count' => 0, 'value' => '' ]; + + if ( $dataItem->getDiType() === DataItem::TYPE_TIME ) { + // max Unix time + $groups[$cellContent]['minDate'] = 2147483647; + $groups[$cellContent]['maxDate'] = 0; + } + } + + $groups[$cellContent]['count'] += $row->count; + + // @TODO complete with all the possible transformations of + // datavalues (DataValues/ValueFormatters) + // based on $printRequest->getOutputFormat() + // and provide to the API the information to + // rebuild the query when values are grouped + // by the output of the printout format, e.g. + // if grouped by unit (for number datatype) + // value should be *, for datetime see the + // method below + + switch( $dataItem->getDiType() ) { + case DataItem::TYPE_NUMBER: + if ( $outputFormat === '-u' ) { + $value = '*'; + } else { + $value = $dataValue->getNumber(); + } + break; + + case DataItem::TYPE_BLOB: + // @see IntlNumberFormatter + // $requestedLength = intval( $outputFormat ); + $value = $dataValue->getWikiValue(); + break; + + case DataItem::TYPE_BOOLEAN: + $value = $dataValue->getWikiValue(); + break; + + case DataItem::TYPE_URI: + $value = $dataValue->getWikiValue(); + break; + + case DataItem::TYPE_TIME: + $currentDate = $dataItem->asDateTime()->getTimestamp(); + $value = $dataValue->getISO8601Date(); + if ( $currentDate < $groups[$cellContent]['minDate'] ) { + $groups[$cellContent]['minDate'] = $currentDate; + } + if ( $currentDate > $groups[$cellContent]['maxDate'] ) { + $groups[$cellContent]['maxDate'] = $currentDate; + } + break; + + case DataItem::TYPE_GEO: + $value = $dataValue->getWikiValue(); + break; + + case DataItem::TYPE_CONTAINER: + $value = $dataValue->getWikiValue(); + break; + + case DataItem::TYPE_WIKIPAGE: + $title_ = $dataValue->getTitle(); + if ( $title_ ) { + $value = $title_->getFullText(); + } else { + $value = $dataValue->getWikiValue(); + $this->searchPanesLog[] = [ + 'canonicalLabel' => $printRequest->getCanonicalLabel(), + 'error' => 'TYPE_WIKIPAGE title is null', + 'wikiValue' => $value, + ]; + } + break; + + case DataItem::TYPE_CONCEPT: + $value = $dataValue->getWikiValue(); + break; + + case DataItem::TYPE_PROPERTY: + + break; + case DataItem::TYPE_NOTYPE: + $value = $dataValue->getWikiValue(); + break; + + default: + $value = $dataValue->getWikiValue(); + + } + + $groups[$cellContent]['value'] = $value; + } + + if ( $outputFormat ) { + $binLength = count( $groups ); + $uniqueRatio = $binLength / $dataLength; + + $this->searchPanesLog[] = [ + 'canonicalLabel' => $printRequest->getCanonicalLabel(), + 'dataLength' => $dataLength, + 'binLength' => $binLength, + 'uniqueRatio' => $uniqueRatio, + 'threshold' => $threshold, + 'grouped' => true, + ]; + + // || $binLength <= 1 + if ( $uniqueRatio > $threshold ) { + return []; + } + + } + + arsort( $groups, SORT_NUMERIC ); + + $ret = []; + foreach( $groups as $content => $value ) { + + // @see https://www.semantic-mediawiki.org/wiki/Help:Search_operators + // the latest value is returned, with the largest range + if ( array_key_exists( 'minDate', $value ) && $value['minDate'] != $value['maxDate'] ) { + // ISO 8601 + // @TODO use a symbol instead and transform from the API + $value['value'] = '>' . date( 'c', $value['minDate'] ) . ']][[' . $printRequest->getCanonicalLabel() . '::<' . date( 'c', $value['maxDate'] ); + } + + $ret[] = [ + 'label' => $content, + 'count' => $value['count'], + 'value' => $value['value'] + ]; + } + + return $ret; + } + + /** + * @see ByGroupPropertyValuesLookup + * @param DIProperty $property + * @param string $p_alias + * @param string $propTypeId + * @return array + */ + private function fetchValuesByGroup( DIProperty $property, $p_alias, $propTypeId ) { + $tableid = $this->datatables->store->findPropertyTableID( $property ); + // $entityIdManager = $this->store->getObjectIds(); + + $proptables = $this->datatables->store->getPropertyTables(); + + // || $subjects === [] + if ( $tableid === '' || !isset( $proptables[$tableid] ) ) { + return []; + } + + $connection = $this->datatables->store->getConnection( 'mw.db' ); + + $propTable = $proptables[$tableid]; + $isIdField = false; + + $diHandler = $this->datatables->store->getDataItemHandlerForDIType( + $propTable->getDiType() + ); + + foreach ( $diHandler->getFetchFields() as $field => $fieldType ) { + if ( !$isIdField && $fieldType === FieldType::FIELD_ID ) { + $isIdField = true; + } + } + + $groupBy = $diHandler->getLabelField(); + $pid = ''; + + if ( $groupBy === '' ) { + $groupBy = $diHandler->getIndexField(); + } + + $groupBy = "$p_alias.$groupBy"; + $orderBy = "count DESC, $groupBy ASC"; + + $diType = $propTable->getDiType(); + + if ( $diType === DataItem::TYPE_WIKIPAGE ) { + $fields = [ + "i.smw_id", + "i.smw_title", + "i.smw_namespace", + "i.smw_iw", + "i.smw_subobject", + "i.smw_hash", + "i.smw_sort", + "COUNT( $groupBy ) as count" + ]; + + $groupBy = "$p_alias.o_id, i.smw_id"; + $orderBy = "count DESC, i.smw_sort ASC"; + } elseif ( $diType === DataItem::TYPE_BLOB ) { + $fields = [ "$p_alias.o_hash, $p_alias.o_blob", "COUNT( $p_alias.o_hash ) as count" ]; + + // @see DIBlobHandler + $groupBy = ( $propTypeId !== '_keyw' ? "$p_alias.o_hash, $p_alias.o_blob" + : "$p_alias.o_hash" ); + + } elseif ( $diType === DataItem::TYPE_URI ) { + $fields = [ "$p_alias.o_serialized, $p_alias.o_blob", "COUNT( $p_alias.o_serialized ) as count" ]; + $groupBy = "$p_alias.o_serialized, $p_alias.o_blob"; + } elseif ( $diType === DataItem::TYPE_NUMBER ) { + $fields = [ "$p_alias.o_serialized,$p_alias.o_sortkey, COUNT( $p_alias.o_serialized ) as count" ]; + $groupBy = "$p_alias.o_serialized,$p_alias.o_sortkey"; + $orderBy = "count DESC, $p_alias.o_sortkey DESC"; + } else { + $fields = [ "$groupBy", "COUNT( $groupBy ) as count" ]; + } + + // if ( !$propTable->isFixedPropertyTable() ) { + // $pid = $entityIdManager->getSMWPropertyID( $property ); + // } + + return [ $diType, $isIdField, $fields, $groupBy, $orderBy ]; + } + + /** + * @param PrintRequest $printRequest + * @param array $searchPanesOptions + * @param array $searchPanesParameterOptions + * @return array + */ + private function searchPanesMainlabel( $printRequest, $searchPanesOptions, $searchPanesParameterOptions ) { + + // mainlabel consists only of unique values, + // so do not display if settings don't allow that + if ( $searchPanesOptions['minCount'] > 1 ) { + return []; + } + + $threshold = !empty( $searchPanesParameterOptions['threshold'] ) ? + $searchPanesParameterOptions['threshold'] : $searchPanesOptions['threshold']; + + $this->searchPanesLog[] = [ + 'canonicalLabel' => 'mainLabel', + 'threshold' => $threshold, + ]; + + if ( $threshold < 1 ) { + return []; + } + + $query = $this->datatables->query; + $queryDescription = $query->getDescription(); + $queryDescription->setPrintRequests( [] ); + + $conditionBuilder = $this->queryEngineFactory->newConditionBuilder(); + $rootid = $conditionBuilder->buildCondition( $query ); + + \SMW\SQLStore\QueryEngine\QuerySegment::$qnum = 0; + $querySegmentList = $conditionBuilder->getQuerySegmentList(); + + $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor(); + + $querySegmentListProcessor->setQuerySegmentList( $querySegmentList ); + + // execute query tree, resolve all dependencies + $querySegmentListProcessor->process( $rootid ); + + $qobj = $querySegmentList[$rootid]; + + global $smwgQMaxLimit; + + $sql_options = [ + // *** should we set a limit here ? + // it makes sense to show the pane for + // mainlabel only when page titles are grouped + // through the printout format or even the printout template + // 'LIMIT' => $smwgQMaxLimit, + // title + 'ORDER BY' => 't' + ]; + + // Selecting those is required in standard SQL (but MySQL does not require it). + $sortfields = implode( ',', $qobj->sortfields ); + $sortfields = $sortfields ? ',' . $sortfields : ''; + + // @see QueryEngine + $res = $this->connection->select( + $this->connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from, + "$qobj->alias.smw_id AS id," . + "$qobj->alias.smw_title AS t," . + "$qobj->alias.smw_namespace AS ns," . + "$qobj->alias.smw_iw AS iw," . + "$qobj->alias.smw_subobject AS so," . + "$qobj->alias.smw_sortkey AS sortkey" . + "$sortfields", + $qobj->where, + __METHOD__, + $sql_options + ); + + $diHandler = $this->datatables->store->getDataItemHandlerForDIType( + DataItem::TYPE_WIKIPAGE + ); + + $outputMode = SMW_OUTPUT_HTML; + $isSubject = false; + $groups = []; + foreach( $res as $row) { + $dataItem = $diHandler->dataItemFromDBKeys( [ + $row->t, + intval( $row->ns ), + $row->iw, + '', + $row->so + ] ); + + $dataValue = DataValueFactory::getInstance()->newDataValueByItem( + $dataItem + ); + + if ( $printRequest->getOutputFormat() ) { + $dataValue->setOutputFormat( $printRequest->getOutputFormat() ); + } + + $cellContent = $this->datatables->getCellContent( + $printRequest->getCanonicalLabel(), + [ $dataValue ], + $outputMode, + $isSubject + ); + + if ( !array_key_exists( $cellContent, $groups ) ) { + $groups[$cellContent] = [ 'count' => 0, 'value' => '' ]; + } + + $groups[$cellContent]['count']++; + $groups[$cellContent]['value'] = $dataValue->getTitle()->getText(); + } + + arsort( $groups, SORT_NUMERIC ); + + $ret = []; + foreach( $groups as $content => $value ) { + $ret[] = [ + 'label' => $content, + 'value' => $value['value'], + 'count' => $value['count'] + ]; + } + + return $ret; + } + +} diff --git a/formats/datatables/resources/ext.srf.formats.datatables.css b/formats/datatables/resources/ext.srf.formats.datatables.css index 177500a02..fb0afddc1 100644 --- a/formats/datatables/resources/ext.srf.formats.datatables.css +++ b/formats/datatables/resources/ext.srf.formats.datatables.css @@ -35,4 +35,15 @@ right: 8px; } +div.dataTables_processing { + /* background: white; */ + /* box-shadow: 0px 2px 18px 0px rgba(0,0,0,0.12); */ + z-index: 1000; + position: sticky !important; +} + +div.dataTables_processing>div:last-child>div { + background: rgb(13,110,253); +} + diff --git a/formats/datatables/resources/ext.srf.formats.datatables.js b/formats/datatables/resources/ext.srf.formats.datatables.js index b7056f3cc..b5d7344c2 100644 --- a/formats/datatables/resources/ext.srf.formats.datatables.js +++ b/formats/datatables/resources/ext.srf.formats.datatables.js @@ -84,10 +84,10 @@ * @private * @static * - * @param {Object} context + * @param {Object} table */ - initColumnSort: function (context) { - var column = context.data("column-sort"); + initColumnSort: function (table) { + var column = table.data("column-sort"); var order = []; @@ -117,10 +117,10 @@ }); if (order.length > 0) { - context.data("order", order); + table.data("order", order); } else { // default @see https://datatables.net/reference/option/order - context.data("order", [[0, "asc"]]); + table.data("order", [[0, "asc"]]); } }, @@ -148,6 +148,8 @@ } }, + // this is used only if Ajax is disabled and + // the table does not have fields with multiple values getPanesOptions: function (data, columnDefs, options) { var ret = {}; var dataLength = {}; @@ -287,12 +289,53 @@ return searchPanesOptions; }, - parse: { - // ... - }, + callApi: function ( + data, + callback, + preloadData, + searchPanesOptions, + displayLog + ) { + var payload = { + action: "ext.srf.datatables.api", + format: "json", + data: JSON.stringify(data), + }; - exportlinks: function (context, data) { - // ... + new mw.Api() + .post(payload) + .done(function (results) { + var json = results["datatables-json"]; + + if (displayLog) { + console.log("results log", json.log); + } + + // cache all retrieved rows for each sorting + // dimension (column/dir), up to a fixed + // threshold (_cacheLimit) + + if (data.datatableData.search.value === "") { + preloadData[json.cacheKey] = { + data: preloadData[json.cacheKey]["data"] + .slice(0, data.datatableData.start) + .concat(json.data), + count: json.recordsFiltered, + }; + } + + // we retrieve more than "length" + // expected by datatables, so return the + // sliced result + json.data = json.data.slice(0, data.datatableData.datalength); + json.searchPanes = { + options: searchPanesOptions, + }; + callback(json); + }) + .fail(function (error) { + console.log("error", error); + }); }, /** @@ -323,25 +366,16 @@ sInfoThousands: mw.msg("srf-ui-datatables-label-sInfoThousands"), sLengthMenu: mw.msg("srf-ui-datatables-label-sLengthMenu"), sLoadingRecords: mw.msg("srf-ui-datatables-label-sLoadingRecords"), - sProcessing: mw.msg("srf-ui-datatables-label-sProcessing"), + + // *** hide "processing" label above the indicator + // sProcessing: mw.msg("srf-ui-datatables-label-sProcessing"), + sSearch: mw.msg("srf-ui-datatables-label-sSearch"), sZeroRecords: mw.msg("srf-ui-datatables-label-sZeroRecords"), }, - /** - * UI components - * - * @private - * @param {array} context - * @param {array} container - * @param {array} data - */ - ui: function (context, container, data) { - // ... - }, - - // we don't need it anymore, however keep is as - // a reference for alternate use + // we don't need it anymore, however keep it as + // a reference for other use showNotice: function (context, container, msg) { var cookieKey = "srf-ui-datatables-searchpanes-notice-" + @@ -436,37 +470,18 @@ * * @since 1.9 * - * @param {array} context * @param {array} container * @param {array} data */ - init: function (context, container, data) { + init: function (container, data) { var self = this; - // Hide loading spinner - context.find(".srf-loading-dots").hide(); - - // Show container - container.css({ display: "block" }); - - _datatables.initColumnSort(context); - - var order = context.data("order"); + var table = container.find("table"); + table.removeClass("wikitable"); - // Setup a raw table - container.html( - html.element("table", { - style: "width: 100%", - class: - context.data("theme") === "bootstrap" - ? "bordered-table zebra-striped" - : "display", // nowrap - cellpadding: "0", - cellspacing: "0", - border: "0", - }) - ); + _datatables.initColumnSort(table); + var order = table.data("order"); var options = data["formattedOptions"]; // add the button placeholder if any button is required @@ -489,7 +504,10 @@ } var queryResult = data.query.result; - var useAjax = context.data("useAjax"); + var useAjax = table.data("useAjax"); + var count = parseInt(table.data("count")); + + // var mark = isObject(options.mark); var searchPanes = isObject(options.searchPanes); @@ -501,6 +519,36 @@ options.dom = options.dom.replace("P", ""); } + var searchBuilder = options.searchBuilder; + + if (searchBuilder) { + if (options.dom.indexOf("Q") === -1) { + options.dom = "Q" + options.dom; + } + + // @see https://datatables.net/extensions/searchbuilder/customConditions.html + // @see https://github.com/DataTables/SearchBuilder/blob/master/src/searchBuilder.ts + options.searchBuilder = { + depthLimit: 1, + conditions: { + html: { + null: null, + }, + string: { + null: null, + }, + date: { + null: null, + }, + num: { + null: null, + }, + }, + }; + } else { + options.dom = options.dom.replace("Q", ""); + } + // add the pagelength at the proper place in the length menu if ($.inArray(options.pageLength, options.lengthMenu) < 0) { options.lengthMenu.push(options.pageLength); @@ -510,19 +558,20 @@ } var query = data.query.ask; - var printouts = context.data("printouts"); + var printouts = table.data("printouts"); var queryString = query.conditions; - var printrequests = context.data("printrequests"); + var printrequests = table.data("printrequests"); var searchPanesOptions = data.searchPanes; + var searchPanesLog = data.searchPanesLog; - var displayLog = mw.config.get("performer") === context.data("editor"); + var displayLog = mw.config.get("performer") === table.data("editor"); if (displayLog) { console.log("searchPanesLog", searchPanesLog); } - var entityCollation = context.data("collation"); + var entityCollation = table.data("collation"); var columnDefs = []; $.map(printrequests, function (property, index) { @@ -545,6 +594,10 @@ name: printrequests[index].key !== "" ? printouts[index][1] : "", className: "smwtype" + property.typeid, targets: [index], + + // @FIXME https://datatables.net/reference/option/columns.searchBuilderType + // implement in the proper way + searchBuilderType: "string", }, options.columns, data.printoutsParametersOptions[index] @@ -557,20 +610,10 @@ if (searchPanes) { _datatables.initSearchPanesColumns(columnDefs, options); - // @TODO remove "useAjax" and use the following trick - // https://github.com/Knowledge-Wiki/SemanticResultFormats/blob/2230aa3eb8e65dd33ff493ba81269689f50d2945/formats/datatables/resources/ext.srf.formats.datatables.js - // to use searchPanesOptions created server-side when Ajax is - // not required, unfortunately we cannot use the function - // described here https://datatables.net/reference/option/columns.searchPanes.options - // with the searchPanes data retrieved server-side, since - // we cannot simply provide count, label, and value in the searchPanesOptions - // (since is not allowed by the Api) -- however the current solution - // works fine in most cases - if ( - // options["searchPanes"]["forceClient"] || - !useAjax || - !Object.keys(searchPanesOptions).length - ) { + // *** this should now be true only if ajax is + // disabled and the table has no fields with + // multiple values + if (!Object.keys(searchPanesOptions).length) { searchPanesOptions = _datatables.getPanesOptions( queryResult, columnDefs, @@ -586,38 +629,62 @@ } } + // ***important !! this has already + // been used for columnDefs initialization ! + // otherwise the table won't sort !! + delete options.columns; + var conf = $.extend(options, { columnDefs: columnDefs, language: _datatables.oLanguage, order: order, search: { - caseInsensitive: context.data("nocase"), + caseInsensitive: table.data("nocase"), }, + initComplete: function () { + $(container).find(".datatables-spinner").hide(); + } }); // cacheKey ensures that the cached pages // are related to current sorting and searchPanes filters var getCacheKey = function (obj) { - return ( - JSON.stringify(obj.order) + - (!searchPanes - ? "" - : JSON.stringify( - Object.keys(obj.searchPanes).length - ? obj.searchPanes - : Object.fromEntries( - Object.keys(columnDefs).map((x) => [x, {}]) - ) - )) - ); + // this ensures that the preload key + // and the dynamic key match + // this does not work: "searchPanes" in obj && Object.entries(obj.searchPanes).find(x => Object.keys(x).length ) ? obj.searchPanes : {}, + if ("searchPanes" in obj) { + for (var i in obj.searchPanes) { + if (!Object.keys(obj.searchPanes[i]).length) { + delete obj.searchPanes[i]; + } + } + } + + return objectHash.sha1({ + order: obj.order, + // search: obj.search, + searchPanes: + "searchPanes" in obj && + Object.entries(obj.searchPanes).find((x) => Object.keys(x).length) + ? obj.searchPanes + : {}, + searchBuilder: "searchBuilder" in obj ? obj.searchBuilder : {}, + }); }; + if ((searchPanes || searchBuilder) && table.data("multiple-values")) { + useAjax = true; + } + if (!useAjax) { conf.serverSide = false; conf.data = queryResult; // use Ajax only when required } else { + // prevents double spinner + $(container).find(".datatables-spinner").hide(); + var preloadData = {}; // cache using the column index and sorting @@ -627,26 +694,21 @@ order: order.map((x) => { return { column: x[0], dir: x[1] }; }), - searchPanes: {}, }); preloadData[cacheKey] = { data: queryResult, - count: context.data("count"), + count: count, }; - var payload = { - action: "ext.srf.datatables.api", - format: "json", - query: queryString, - columndefs: JSON.stringify(columnDefs), - printouts: JSON.stringify(printouts), - printrequests: JSON.stringify(printrequests), - settings: JSON.stringify( - $.extend( - { count: context.data("count"), displayLog: displayLog }, - query.parameters - ) + var payloadData = { + queryString, + columnDefs, + printouts, + printrequests, + settings: $.extend( + { count: count, displayLog: displayLog }, + query.parameters ), }; @@ -658,16 +720,15 @@ // instead we use the following hack: the Ajax function returns // the preloaded data as long they are available for the requested // slice, and then it uses an ajax call for not available data. - // deferLoading: context.data("count"), - + // deferLoading: table.data("count"), processing: true, serverSide: true, ajax: function (datatableData, callback, settings) { - // must match cacheKey - var key = getCacheKey(datatableData); + // must match initial cacheKey + var cacheKey = getCacheKey(datatableData); - if (!(key in preloadData)) { - preloadData[key] = { data: [] }; + if (!(cacheKey in preloadData)) { + preloadData[cacheKey] = { data: [] }; } // returned cached data for the required @@ -675,22 +736,21 @@ if ( datatableData.search.value === "" && datatableData.start + datatableData.length <= - preloadData[key]["data"].length + preloadData[cacheKey]["data"].length ) { return callback({ draw: datatableData.draw, - data: preloadData[key]["data"].slice( + data: preloadData[cacheKey]["data"].slice( datatableData.start, datatableData.start + datatableData.length ), - recordsTotal: context.data("count"), - recordsFiltered: preloadData[key]["count"], + recordsTotal: count, + recordsFiltered: preloadData[cacheKey]["count"], searchPanes: { options: searchPanesOptions, }, }); } - // flush cache each 40,000 rows // *** another method is to compute the actual // size in bytes of each row, but it takes more @@ -704,53 +764,21 @@ } } - new mw.Api() - .post( - $.extend(payload, { - datatable: JSON.stringify(datatableData), - }) - ) - .done(function (results) { - var json = results["datatables-json"]; - - if (displayLog) { - console.log("results log", json.log); - } - - // cache all retrieved rows for each sorting - // dimension (column/dir), up to a fixed - // threshold (_cacheLimit) - - if (datatableData.search.value === "") { - preloadData[key] = { - data: preloadData[key]["data"] - .slice(0, datatableData.start) - .concat(json.data), - count: json.recordsFiltered, - }; - } - - // we retrieve more than "length" - // expected by datatables, so return the - // sliced result - json.data = json.data.slice(0, datatableData.length); - json.searchPanes = { - options: searchPanesOptions, - }; - callback(json); - }) - .fail(function (error) { - console.log("error", error); - }); + _datatables.callApi( + $.extend(payloadData, { + datatableData, + cacheKey, + }), + callback, + preloadData, + searchPanesOptions, + displayLog + ); }, }); } - // console.log("conf", conf); - container.find("table").DataTable(conf); - }, - update: function (context, data) { - // ... + table.DataTable(conf); }, test: { @@ -766,13 +794,10 @@ var datatables = new srf.formats.datatables(); $(document).ready(function () { - $(".srf-datatable").each(function () { - var context = $(this), - container = context.find(".datatables-container"); - + $(".datatables-container").each(function () { + var container = $(this); var data = JSON.parse(_datatables.getData(container)); - - datatables.init(context, container, data); + datatables.init(container, data); }); }); })(jQuery, mediaWiki, semanticFormats); diff --git a/resources/jquery/datatables/datatables.mark.min.css b/resources/jquery/datatables/datatables.mark.min.css new file mode 100644 index 000000000..722870b7b --- /dev/null +++ b/resources/jquery/datatables/datatables.mark.min.css @@ -0,0 +1 @@ +mark{background:orange;color:black;} diff --git a/resources/jquery/datatables/datatables.mark.min.js b/resources/jquery/datatables/datatables.mark.min.js new file mode 100644 index 000000000..4220b8a01 --- /dev/null +++ b/resources/jquery/datatables/datatables.mark.min.js @@ -0,0 +1,7 @@ +/*!*************************************************** + * datatables.mark.js v2.1.0 + * https://github.com/julmot/datatables.mark.js + * Copyright (c) 2016–2020, Julian Kühnel + * Released under the MIT license https://git.io/voRZ7 + *****************************************************/ +"use strict";var _createClass=function(){function a(t,e){for(var n=0;nt.intervalThreshold?(clearTimeout(n),n=setTimeout(function(){t.mark()},t.intervalMs)):t.mark()}),this.instance.on("destroy",function(){t.instance.off(e)}),this.mark()}},{key:"mark",value:function(){var a=this,r=this.instance.search(),t=i(this.instance.table().body());t.unmark(this.options),this.instance.table().rows({search:"applied"}).data().length&&t.mark(r,this.options),this.instance.columns({search:"applied",page:"current"}).nodes().each(function(t,e){var n=a.instance.column(e).search()||r;n&&t.forEach(function(t){i(t).unmark(a.options).mark(n,a.options)})})}}]),n);function n(t,e){if(_classCallCheck(this,n),!i.fn.mark||!i.fn.unmark)throw new Error("jquery.mark.js is necessary for datatables.mark.js");this.instance=t,this.options="object"===(void 0===e?"undefined":_typeof(e))?e:{},this.intervalThreshold=49,this.intervalMs=300,this.initMarkListener()}i(e).on("init.dt.dth",function(t,e){var n,a;"dt"===t.namespace&&(a=null,(n=i.fn.dataTable.Api(e)).init().mark?a=n.init().mark:i.fn.dataTable.defaults.mark&&(a=i.fn.dataTable.defaults.mark),null!==a&&new r(n,a))})},window,document); \ No newline at end of file diff --git a/resources/jquery/datatables/jquery.mark.min.js b/resources/jquery/datatables/jquery.mark.min.js new file mode 100644 index 000000000..72813bdb2 --- /dev/null +++ b/resources/jquery/datatables/jquery.mark.min.js @@ -0,0 +1,13 @@ +/*!*************************************************** +* mark.js v9.0.0 +* https://markjs.io/ +* Copyright (c) 2014–2018, Julian Kühnel +* Released under the MIT license https://git.io/vwTVl +*****************************************************/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],t):e.Mark=t(e.jQuery)}(this,function(e){"use strict";function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;n(this,e),this.ctx=t,this.iframes=r,this.exclude=o,this.iframesTimeout=i}return o(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach(function(t){var n=e.filter(function(e){return e.contains(t)}).length>0;-1!==e.indexOf(t)||n||e.push(t)}),e}},{key:"getIframeContents",value:function(e,t){var n,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){};try{var o=e.contentWindow;if(n=o.document,!o||!n)throw new Error("iframe inaccessible")}catch(e){r()}n&&t(n)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,o=!1,i=null,a=function a(){if(!o){o=!0,clearTimeout(i);try{r.isIframeBlank(e)||(e.removeEventListener("load",a),r.getIframeContents(e,t,n))}catch(e){n()}}};e.addEventListener("load",a),i=setTimeout(a,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(e){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,function(){return!0},function(e){r++,n.waitForIframes(e.querySelector("html"),function(){--r||t()})},function(e){e||t()})}},{key:"forEachIframe",value:function(t,n,r){var o=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},a=t.querySelectorAll("iframe"),s=a.length,c=0;a=Array.prototype.slice.call(a);var u=function(){--s<=0&&i(c)};s||u(),a.forEach(function(t){e.matches(t,o.exclude)?u():o.onIframeReady(t,function(e){n(t)&&(c++,r(e)),u()},u)})}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:null===t?e.nextNode():e.nextNode()&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var o=!1,i=!1;return r.forEach(function(e,t){e.val===n&&(o=t,i=e.handled)}),this.compareNodeIframe(e,t,n)?(!1!==o||i?!1===o||i||(r[o].handled=!0):r.push({val:n,handled:!0}),!0):(!1===o&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var o=this;e.forEach(function(e){e.handled||o.getIframeContents(e.val,function(e){o.createInstanceOnIframe(e).forEachNode(t,n,r)})})}},{key:"iterateThroughNodes",value:function(e,t,n,r,o){for(var i,a,s,c=this,u=this.createIterator(t,e,r),l=[],h=[];s=void 0,s=c.getIteratorNode(u),a=s.prevNode,i=s.node;)this.iframes&&this.forEachIframe(t,function(e){return c.checkIframeFilter(i,a,e,l)},function(t){c.createInstanceOnIframe(t).forEachNode(e,function(e){return h.push(e)},r)}),h.push(i);h.forEach(function(e){n(e)}),this.iframes&&this.handleOpenIframes(l,e,n,r),o()}},{key:"forEachNode",value:function(e,t,n){var r=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},i=this.getContexts(),a=i.length;a||o(),i.forEach(function(i){var s=function(){r.iterateThroughNodes(e,i,t,n,function(){--a<=0&&o()})};r.iframes?r.waitForIframes(i,s):s()})}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var o=!1;return n.every(function(t){return!r.call(e,t)||(o=!0,!1)}),o}return!1}}]),e}(),s= +/* */ +function(){function e(t){n(this,e),this.opt=i({},{diacritics:!0,synonyms:{},accuracy:"partially",caseSensitive:!1,ignoreJoiners:!1,ignorePunctuation:[],wildcards:"disabled"},t)}return o(e,[{key:"create",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),new RegExp(e,"gm".concat(this.opt.caseSensitive?"":"i"))}},{key:"sortByLength",value:function(e){return e.sort(function(e,t){return e.length===t.length?e>t?1:-1:t.length-e.length})}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createSynonymsRegExp",value:function(e){var t=this,n=this.opt.synonyms,r=this.opt.caseSensitive?"":"i",o=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var i in n)if(n.hasOwnProperty(i)){var a=Array.isArray(n[i])?n[i]:[n[i]];a.unshift(i),(a=this.sortByLength(a).map(function(e){return"disabled"!==t.opt.wildcards&&(e=t.setupWildcardsRegExp(e)),e=t.escapeStr(e)}).filter(function(e){return""!==e})).length>1&&(e=e.replace(new RegExp("(".concat(a.map(function(e){return t.escapeStr(e)}).join("|"),")"),"gm".concat(r)),o+"(".concat(a.map(function(e){return t.processSynonyms(e)}).join("|"),")")+o))}return e}},{key:"processSynonyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,function(e){return"\\"===e.charAt(0)?"?":""})).replace(/(?:\\)*\*/g,function(e){return"\\"===e.charAt(0)?"*":""})}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"})}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("[".concat(t.join(""),"]*")):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"],r=[];return e.split("").forEach(function(o){n.every(function(n){if(-1!==n.indexOf(o)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("[".concat(n,"]"),"gm".concat(t)),"[".concat(n,"]")),r.push(n)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n=this.opt.accuracy,r="string"==typeof n?n:n.value,o="string"==typeof n?[]:n.limiters,i="";switch(o.forEach(function(e){i+="|".concat(t.escapeStr(e))}),r){case"partially":default:return"()(".concat(e,")");case"complementary":return i="\\s"+(i||this.escapeStr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿")),"()([^".concat(i,"]*").concat(e,"[^").concat(i,"]*)");case"exactly":return"(^|\\s".concat(i,")(").concat(e,")(?=$|\\s").concat(i,")")}}}]),e}(),c= +/* */ +function(){function e(t){n(this,e),this.ctx=t,this.ie=!1;var r=window.navigator.userAgent;(r.indexOf("MSIE")>-1||r.indexOf("Trident")>-1)&&(this.ie=!0)}return o(e,[{key:"log",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===t(r)&&"function"==typeof r[n]&&r[n]("mark.js: ".concat(e))}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach(function(e){t.opt.separateWordSearch?e.split(" ").forEach(function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)}):e.trim()&&-1===n.indexOf(e)&&n.push(e)}),{keywords:n.sort(function(e,t){return t.length-e.length}),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort(function(e,t){return e.start-t.start}).forEach(function(e){var o=t.callNoMatchOnInvalidRanges(e,r),i=o.start,a=o.end;o.valid&&(e.start=i,e.length=a-i,n.push(e),r=a)}),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n,r,o=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?o=!0:(this.log("Ignoring invalid or overlapping range: "+"".concat(JSON.stringify(e))),this.opt.noMatch(e))):(this.log("Ignoring invalid range: ".concat(JSON.stringify(e))),this.opt.noMatch(e)),{start:n,end:r,valid:o}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r,o=!0,i=n.length,a=t-i,s=parseInt(e.start,10)-a;return(r=(s=s>i?i:s)+parseInt(e.length,10))>i&&(r=i,this.log("End range automatically set to the max value of ".concat(i))),s<0||r-s<0||s>i||r>i?(o=!1,this.log("Invalid range: ".concat(JSON.stringify(e))),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(o=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:o}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})},function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:n,nodes:r})})}},{key:"matchesExclude",value:function(e){return a.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",o=e.splitText(t),i=o.splitText(n-t),a=document.createElement(r);return a.setAttribute("data-markjs","true"),this.opt.className&&a.setAttribute("class",this.opt.className),a.textContent=o.textContent,o.parentNode.replaceChild(a,o),i}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,o){var i=this;e.nodes.every(function(a,s){var c=e.nodes[s+1];if(void 0===c||c.start>t){if(!r(a.node))return!1;var u=t-a.start,l=(n>a.end?a.end:n)-a.start,h=e.value.substr(0,a.start),f=e.value.substr(l+a.start);if(a.node=i.wrapRangeInTextNode(a.node,u,l),e.value=h+f,e.nodes.forEach(function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=l),e.nodes[n].end-=l)}),n-=l,o(a.node.previousSibling,a.start),!(n>a.end))return!1;t=a.end}return!0})}},{key:"wrapGroups",value:function(e,t,n,r){return r((e=this.wrapRangeInTextNode(e,t,t+n)).previousSibling),e}},{key:"separateGroups",value:function(e,t,n,r,o){for(var i=t.length,a=1;a-1&&r(t[a],e)&&(e=this.wrapGroups(e,s,t[a].length,o))}return e}},{key:"wrapMatches",value:function(e,t,n,r,o){var i=this,a=0===t?0:t+1;this.getTextNodes(function(t){t.nodes.forEach(function(t){var o;for(t=t.node;null!==(o=e.exec(t.textContent))&&""!==o[a];){if(i.opt.separateGroups)t=i.separateGroups(t,o,a,n,r);else{if(!n(o[a],t))continue;var s=o.index;if(0!==a)for(var c=1;c>16),s((65280&n)>>8),s(255&n);return 2==r?s(255&(n=f(e.charAt(t))<<2|f(e.charAt(t+1))>>4)):1==r&&(s((n=f(e.charAt(t))<<10|f(e.charAt(t+1))<<4|f(e.charAt(t+2))>>2)>>8&255),s(255&n)),o},e.fromByteArray=function(e){var t,n,r,o,i=e.length%3,u="";function s(e){return"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(e)}for(t=0,r=e.length-i;t>18&63)+s(o>>12&63)+s(o>>6&63)+s(63&o);switch(i){case 1:u=(u+=s((n=e[e.length-1])>>2))+s(n<<4&63)+"==";break;case 2:u=(u=(u+=s((n=(e[e.length-2]<<8)+e[e.length-1])>>10))+s(n>>4&63))+s(n<<2&63)+"="}return u}}(void 0===f?this.base64js={}:f)}.call(this,e("lYpoI2"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},e("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/node_modules/gulp-browserify/node_modules/base64-js/lib/b64.js","/node_modules/gulp-browserify/node_modules/base64-js/lib")},{buffer:3,lYpoI2:11}],3:[function(O,e,H){!function(e,n,f,r,h,p,g,y,w){var a=O("base64-js"),i=O("ieee754");function f(e,t,n){if(!(this instanceof f))return new f(e,t,n);var r,o,i,u,s=typeof e;if("base64"===t&&"string"==s)for(e=(u=e).trim?u.trim():u.replace(/^\s+|\s+$/g,"");e.length%4!=0;)e+="=";if("number"==s)r=j(e);else if("string"==s)r=f.byteLength(e,t);else{if("object"!=s)throw new Error("First argument needs to be a number, array or string.");r=j(e.length)}if(f._useTypedArrays?o=f._augment(new Uint8Array(r)):((o=this).length=r,o._isBuffer=!0),f._useTypedArrays&&"number"==typeof e.byteLength)o._set(e);else if(C(u=e)||f.isBuffer(u)||u&&"object"==typeof u&&"number"==typeof u.length)for(i=0;i>8,n=n%256,r.push(n),r.push(t);return r}(t),e,n,r)}function v(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;o>>0)):(t+1>>0),o}function _(e,t,n,r){if(r||(d("boolean"==typeof n,"missing or invalid endian"),d(null!=t,"missing offset"),d(t+1>>8*(r?i:1-i)}function l(e,t,n,r,o){o||(d(null!=t,"missing value"),d("boolean"==typeof r,"missing or invalid endian"),d(null!=n,"missing offset"),d(n+3>>8*(r?i:3-i)&255}function B(e,t,n,r,o){o||(d(null!=t,"missing value"),d("boolean"==typeof r,"missing or invalid endian"),d(null!=n,"missing offset"),d(n+1this.length&&(r=this.length);var o=(r=e.length-t=this.length))return this[e]},f.prototype.readUInt16LE=function(e,t){return o(this,e,!0,t)},f.prototype.readUInt16BE=function(e,t){return o(this,e,!1,t)},f.prototype.readUInt32LE=function(e,t){return u(this,e,!0,t)},f.prototype.readUInt32BE=function(e,t){return u(this,e,!1,t)},f.prototype.readInt8=function(e,t){if(t||(d(null!=e,"missing offset"),d(e=this.length))return 128&this[e]?-1*(255-this[e]+1):this[e]},f.prototype.readInt16LE=function(e,t){return _(this,e,!0,t)},f.prototype.readInt16BE=function(e,t){return _(this,e,!1,t)},f.prototype.readInt32LE=function(e,t){return E(this,e,!0,t)},f.prototype.readInt32BE=function(e,t){return E(this,e,!1,t)},f.prototype.readFloatLE=function(e,t){return I(this,e,!0,t)},f.prototype.readFloatBE=function(e,t){return I(this,e,!1,t)},f.prototype.readDoubleLE=function(e,t){return A(this,e,!0,t)},f.prototype.readDoubleBE=function(e,t){return A(this,e,!1,t)},f.prototype.writeUInt8=function(e,t,n){n||(d(null!=e,"missing value"),d(null!=t,"missing offset"),d(t=this.length||(this[t]=e)},f.prototype.writeUInt16LE=function(e,t,n){s(this,e,t,!0,n)},f.prototype.writeUInt16BE=function(e,t,n){s(this,e,t,!1,n)},f.prototype.writeUInt32LE=function(e,t,n){l(this,e,t,!0,n)},f.prototype.writeUInt32BE=function(e,t,n){l(this,e,t,!1,n)},f.prototype.writeInt8=function(e,t,n){n||(d(null!=e,"missing value"),d(null!=t,"missing offset"),d(t=this.length||(0<=e?this.writeUInt8(e,t,n):this.writeUInt8(255+e+1,t,n))},f.prototype.writeInt16LE=function(e,t,n){B(this,e,t,!0,n)},f.prototype.writeInt16BE=function(e,t,n){B(this,e,t,!1,n)},f.prototype.writeInt32LE=function(e,t,n){L(this,e,t,!0,n)},f.prototype.writeInt32BE=function(e,t,n){L(this,e,t,!1,n)},f.prototype.writeFloatLE=function(e,t,n){U(this,e,t,!0,n)},f.prototype.writeFloatBE=function(e,t,n){U(this,e,t,!1,n)},f.prototype.writeDoubleLE=function(e,t,n){x(this,e,t,!0,n)},f.prototype.writeDoubleBE=function(e,t,n){x(this,e,t,!1,n)},f.prototype.fill=function(e,t,n){if(t=t||0,n=n||this.length,d("number"==typeof(e="string"==typeof(e=e||0)?e.charCodeAt(0):e)&&!isNaN(e),"value is not a number"),d(t<=n,"end < start"),n!==t&&0!==this.length){d(0<=t&&t"},f.prototype.toArrayBuffer=function(){if("undefined"==typeof Uint8Array)throw new Error("Buffer.toArrayBuffer not supported in this browser");if(f._useTypedArrays)return new f(this).buffer;for(var e=new Uint8Array(this.length),t=0,n=e.length;t=t.length||o>=e.length);o++)t[o+n]=e[o];return o}function N(e){try{return decodeURIComponent(e)}catch(e){return String.fromCharCode(65533)}}function Y(e,t){d("number"==typeof e,"cannot write a non-number as a number"),d(0<=e,"specified a negative value for writing an unsigned value"),d(e<=t,"value is larger than maximum value for type"),d(Math.floor(e)===e,"value has a fractional component")}function F(e,t,n){d("number"==typeof e,"cannot write a non-number as a number"),d(e<=t,"value larger than maximum allowed value"),d(n<=e,"value smaller than minimum allowed value"),d(Math.floor(e)===e,"value has a fractional component")}function D(e,t,n){d("number"==typeof e,"cannot write a non-number as a number"),d(e<=t,"value larger than maximum allowed value"),d(n<=e,"value smaller than minimum allowed value")}function d(e,t){if(!e)throw new Error(t||"Failed assertion")}f._augment=function(e){return e._isBuffer=!0,e._get=e.get,e._set=e.set,e.get=t.get,e.set=t.set,e.write=t.write,e.toString=t.toString,e.toLocaleString=t.toString,e.toJSON=t.toJSON,e.copy=t.copy,e.slice=t.slice,e.readUInt8=t.readUInt8,e.readUInt16LE=t.readUInt16LE,e.readUInt16BE=t.readUInt16BE,e.readUInt32LE=t.readUInt32LE,e.readUInt32BE=t.readUInt32BE,e.readInt8=t.readInt8,e.readInt16LE=t.readInt16LE,e.readInt16BE=t.readInt16BE,e.readInt32LE=t.readInt32LE,e.readInt32BE=t.readInt32BE,e.readFloatLE=t.readFloatLE,e.readFloatBE=t.readFloatBE,e.readDoubleLE=t.readDoubleLE,e.readDoubleBE=t.readDoubleBE,e.writeUInt8=t.writeUInt8,e.writeUInt16LE=t.writeUInt16LE,e.writeUInt16BE=t.writeUInt16BE,e.writeUInt32LE=t.writeUInt32LE,e.writeUInt32BE=t.writeUInt32BE,e.writeInt8=t.writeInt8,e.writeInt16LE=t.writeInt16LE,e.writeInt16BE=t.writeInt16BE,e.writeInt32LE=t.writeInt32LE,e.writeInt32BE=t.writeInt32BE,e.writeFloatLE=t.writeFloatLE,e.writeFloatBE=t.writeFloatBE,e.writeDoubleLE=t.writeDoubleLE,e.writeDoubleBE=t.writeDoubleBE,e.fill=t.fill,e.inspect=t.inspect,e.toArrayBuffer=t.toArrayBuffer,e}}.call(this,O("lYpoI2"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},O("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/node_modules/gulp-browserify/node_modules/buffer/index.js","/node_modules/gulp-browserify/node_modules/buffer")},{"base64-js":2,buffer:3,ieee754:10,lYpoI2:11}],4:[function(c,d,e){!function(e,t,a,n,r,o,i,u,s){var a=c("buffer").Buffer,f=4,l=new a(f);l.fill(0);d.exports={hash:function(e,t,n,r){for(var o=t(function(e,t){e.length%f!=0&&(n=e.length+(f-e.length%f),e=a.concat([e,l],n));for(var n,r=[],o=t?e.readInt32BE:e.readInt32LE,i=0;is?t=e(t):t.length>5]|=128<>>9<<4)]=t;for(var n=1732584193,r=-271733879,o=-1732584194,i=271733878,u=0;u>>32-o,n)}function c(e,t,n,r,o,i,u){return s(t&n|~t&r,e,t,o,i,u)}function d(e,t,n,r,o,i,u){return s(t&r|n&~r,e,t,o,i,u)}function h(e,t,n,r,o,i,u){return s(t^n^r,e,t,o,i,u)}function p(e,t,n,r,o,i,u){return s(n^(t|~r),e,t,o,i,u)}function g(e,t){var n=(65535&e)+(65535&t);return(e>>16)+(t>>16)+(n>>16)<<16|65535&n}b.exports=function(e){return t.hash(e,n,16)}}.call(this,w("lYpoI2"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},w("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/node_modules/gulp-browserify/node_modules/crypto-browserify/md5.js","/node_modules/gulp-browserify/node_modules/crypto-browserify")},{"./helpers":4,buffer:3,lYpoI2:11}],7:[function(e,l,t){!function(e,t,n,r,o,i,u,s,f){var a;l.exports=a||function(e){for(var t,n=new Array(e),r=0;r>>((3&r)<<3)&255;return n}}.call(this,e("lYpoI2"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},e("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/node_modules/gulp-browserify/node_modules/crypto-browserify/rng.js","/node_modules/gulp-browserify/node_modules/crypto-browserify")},{buffer:3,lYpoI2:11}],8:[function(c,d,e){!function(e,t,n,r,o,s,a,f,l){var i=c("./helpers");function u(l,c){l[c>>5]|=128<<24-c%32,l[15+(c+64>>9<<4)]=c;for(var e,t,n,r=Array(80),o=1732584193,i=-271733879,u=-1732584194,s=271733878,d=-1009589776,h=0;h>16)+(t>>16)+(n>>16)<<16|65535&n}function v(e,t){return e<>>32-t}d.exports=function(e){return i.hash(e,u,20,!0)}}.call(this,c("lYpoI2"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},c("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/node_modules/gulp-browserify/node_modules/crypto-browserify/sha.js","/node_modules/gulp-browserify/node_modules/crypto-browserify")},{"./helpers":4,buffer:3,lYpoI2:11}],9:[function(c,d,e){!function(e,t,n,r,u,s,a,f,l){function b(e,t){var n=(65535&e)+(65535&t);return(e>>16)+(t>>16)+(n>>16)<<16|65535&n}function o(e,l){var c,d=new Array(1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298),t=new Array(1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225),n=new Array(64);e[l>>5]|=128<<24-l%32,e[15+(l+64>>9<<4)]=l;for(var r,o,h=0;h>>t|e<<32-t},v=function(e,t){return e>>>t};d.exports=function(e){return i.hash(e,o,32,!0)}}.call(this,c("lYpoI2"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},c("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/node_modules/gulp-browserify/node_modules/crypto-browserify/sha256.js","/node_modules/gulp-browserify/node_modules/crypto-browserify")},{"./helpers":4,buffer:3,lYpoI2:11}],10:[function(e,t,f){!function(e,t,n,r,o,i,u,s,a){f.read=function(e,t,n,r,o){var i,u,l=8*o-r-1,c=(1<>1,s=-7,a=n?o-1:0,f=n?-1:1,o=e[t+a];for(a+=f,i=o&(1<<-s)-1,o>>=-s,s+=l;0>=-s,s+=r;0>1,d=23===r?Math.pow(2,-24)-Math.pow(2,-77):0,f=n?0:c-1,h=n?1:-1,c=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(i=isNaN(t)?1:0,o=s):(o=Math.floor(Math.log(t)/Math.LN2),t*(n=Math.pow(2,-o))<1&&(o--,n*=2),2<=(t+=1<=o+a?d/n:d*Math.pow(2,1-a))*n&&(o++,n/=2),s<=o+a?(i=0,o=s):1<=o+a?(i=(t*n-1)*Math.pow(2,r),o+=a):(i=t*Math.pow(2,a-1)*Math.pow(2,r),o=0));8<=r;e[l+f]=255&i,f+=h,i/=256,r-=8);for(o=o<.*" ] } }