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 <bernhard.krabina@km-a.net>
这个提交包含在:
thomas-topway-it 2024-03-30 16:53:46 +04:00 提交者 GitHub
父节点 6b4c2f2cf3
当前提交 6dd7e46c19
找不到此签名对应的密钥
GPG 密钥 ID: B5690EEEBB952194
共有 11 个文件被更改,包括 1223 次插入965 次删除

查看文件

@ -1017,10 +1017,14 @@ return [
'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',
]
],

查看文件

@ -31,9 +31,11 @@ class Api extends ApiBase {
// 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 @@ class Api extends ApiBase {
$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 @@ class Api extends ApiBase {
[
// *** 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 @@ class Api extends ApiBase {
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 @@ class Api extends ApiBase {
$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 @@ class Api extends ApiBase {
// 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 @@ class Api extends ApiBase {
}
}
// @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 @@ class Api extends ApiBase {
$queryStr = implode( 'OR', $conditions );
// trigger_error('queryStr ' . $queryStr);
$log['queryStr '] = $queryStr;
$query = SMWQueryProcessor::createQuery(
@ -205,7 +265,6 @@ class Api extends ApiBase {
// $smwgQMaxSize = max( $smwgQMaxSize, $size );
// trigger_error('smwgQMaxSize ' . $smwgQMaxSize);
$applicationFactory = ServicesFactory::getInstance();
$queryEngine = $applicationFactory->getStore();
$results = $queryEngine->getQueryResult( $query );
@ -234,6 +293,8 @@ class Api extends ApiBase {
'data' => $res,
'recordsTotal' => $settings['count'],
'recordsFiltered' => $count,
'cacheKey' => $data['cacheKey'],
'datalength' => $datatableData['length']
];
if ( $settings['displayLog'] ) {
@ -275,44 +336,10 @@ class Api extends ApiBase {
*/
protected function getAllowedParams() {
return [
'query' => [
'data' => [
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true,
],
'columndefs' => [
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',
]
];
}

文件差异内容过多而无法显示 加载差异

查看文件

@ -0,0 +1,746 @@
<?php
/**
* SRF DataTables and SMWAPI.
*
* @see http://datatables.net/
*
* @licence GPL-2.0-or-later
* @author thomas-topway-it for KM-A
*/
namespace SRF\DataTables;
use SMW\DIWikiPage;
use SMW\DataValueFactory;
use SMW\DataTypeRegistry;
use SMW\DIProperty;
use SMW\SQLStore\SQLStore;
use SMW\SQLStore\TableBuilder\FieldType;
use SMW\QueryFactory;
use SMW\Services\ServicesFactory as ApplicationFactory;
use SMWDataItem as DataItem;
use SMWQueryProcessor;
use SMWPrintRequest;
use SMW\Query\PrintRequest;
use SMW\SQLStore\QueryEngineFactory;
class SearchPanes {
/** @var array */
private $searchPanesLog = [];
private $queryEngineFactory;
private $datatables;
private $connection;
private $queryFactory;
public function __construct( $datatables ) {
$this->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 <<and t3.s_id = smw_object_ids.smw_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;
}
}

查看文件

@ -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);
}

查看文件

@ -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();
var table = container.find("table");
table.removeClass("wikitable");
// Show container
container.css({ display: "block" });
_datatables.initColumnSort(context);
var order = context.data("order");
// 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);

查看文件

@ -0,0 +1 @@
mark{background:orange;color:black;}

查看文件

@ -0,0 +1,7 @@
/*!***************************************************
* datatables.mark.js v2.1.0
* https://github.com/julmot/datatables.mark.js
* Copyright (c) 20162020, 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;n<e.length;n++){var a=e[n];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(t,a.key,a)}}return function(t,e,n){return e&&a(t.prototype,e),n&&a(t,n),t}}(),_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}!function(e,t,n){var a;"object"===("undefined"==typeof exports?"undefined":_typeof(exports))?(a=require("jquery"),require("datatables.net"),require("mark.js/dist/jquery.mark.js"),module.exports=e(0,n,a)):"function"==typeof define&&define.amd?define(["jquery","datatables.net","markjs"],function(t){return e(0,n,t)}):e(0,n,jQuery)}(function(t,e,i){var r=(_createClass(n,[{key:"initMarkListener",value:function(){var t=this,e="draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth";e+=" responsive-display.dt.dth";var n=null;this.instance.on(e,function(){t.instance.rows({filter:"applied",page:"current"}).nodes().length>t.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);

文件差异因一行或多行过长而隐藏

文件差异因一行或多行过长而隐藏

查看文件

@ -26,7 +26,7 @@
"subject": "Test/Datatables/Q.1",
"assert-output": {
"to-contain": [
"<div .*data-collation.*data-datatables.*data-editor.*datatables-container"
"<div .*class=\"datatables-container\">.*<table class=\"srf-datatable.*data-collation.*data-printrequests.*>"
]
}
}